@pixerate/editor 0.113.0 → 0.115.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 (125) hide show
  1. package/THIRD_PARTY_NOTICES.md +8 -0
  2. package/dist/animation-editor/index.cjs +60 -54
  3. package/dist/animation-editor/index.cjs.map +1 -1
  4. package/dist/animation-editor/index.d.cts +3 -3
  5. package/dist/animation-editor/index.d.ts +3 -3
  6. package/dist/animation-editor/index.js +14 -8
  7. package/dist/animation-editor/index.js.map +1 -1
  8. package/dist/builtin-CY97ppKM.d.cts +354 -0
  9. package/dist/builtin-CY97ppKM.d.ts +354 -0
  10. package/dist/canvas/index.cjs +3 -3
  11. package/dist/canvas/index.cjs.map +1 -1
  12. package/dist/canvas/index.js +9 -9
  13. package/dist/chunk-26PGXHY7.cjs +753 -0
  14. package/dist/chunk-26PGXHY7.cjs.map +1 -0
  15. package/dist/chunk-2OJACTPR.cjs +929 -0
  16. package/dist/chunk-2OJACTPR.cjs.map +1 -0
  17. package/dist/chunk-3UFGNHSP.js +929 -0
  18. package/dist/chunk-3UFGNHSP.js.map +1 -0
  19. package/dist/chunk-643B2HKC.cjs +534 -0
  20. package/dist/chunk-643B2HKC.cjs.map +1 -0
  21. package/dist/chunk-6FCKOS2B.js +534 -0
  22. package/dist/chunk-6FCKOS2B.js.map +1 -0
  23. package/dist/chunk-BRLYOPLT.js +2574 -0
  24. package/dist/chunk-BRLYOPLT.js.map +1 -0
  25. package/dist/{chunk-LQEEDHOP.js → chunk-C6C5PDKU.js} +759 -761
  26. package/dist/chunk-C6C5PDKU.js.map +1 -0
  27. package/dist/{chunk-4KLT7ZYS.cjs → chunk-DFQEX3AM.cjs} +29 -29
  28. package/dist/{chunk-4KLT7ZYS.cjs.map → chunk-DFQEX3AM.cjs.map} +1 -1
  29. package/dist/chunk-G5NMLUVF.cjs +2574 -0
  30. package/dist/chunk-G5NMLUVF.cjs.map +1 -0
  31. package/dist/{chunk-YGIMK4NX.cjs → chunk-I7FQBWMQ.cjs} +813 -842
  32. package/dist/chunk-I7FQBWMQ.cjs.map +1 -0
  33. package/dist/{chunk-LPYOIVLA.js → chunk-LKCYANMW.js} +3 -3
  34. package/dist/{chunk-W3GXSUSQ.cjs → chunk-LPLCWZTM.cjs} +5087 -1283
  35. package/dist/chunk-LPLCWZTM.cjs.map +1 -0
  36. package/dist/chunk-MMVHBKMM.js +753 -0
  37. package/dist/chunk-MMVHBKMM.js.map +1 -0
  38. package/dist/{chunk-POGBUYWE.cjs → chunk-ST7E3NEE.cjs} +857 -859
  39. package/dist/chunk-ST7E3NEE.cjs.map +1 -0
  40. package/dist/{chunk-7VOHOPLW.cjs → chunk-TCKIXI46.cjs} +59 -59
  41. package/dist/{chunk-7VOHOPLW.cjs.map → chunk-TCKIXI46.cjs.map} +1 -1
  42. package/dist/{chunk-XIOMMGPD.cjs → chunk-TFOPHZ3U.cjs} +1 -1
  43. package/dist/chunk-TFOPHZ3U.cjs.map +1 -0
  44. package/dist/{chunk-Z4BC6M74.js → chunk-UBIXVXPR.js} +2 -2
  45. package/dist/{chunk-BLWCAQCW.js → chunk-VXR7XWMF.js} +4574 -770
  46. package/dist/chunk-VXR7XWMF.js.map +1 -0
  47. package/dist/{chunk-CZOXTEMN.js → chunk-XICA4PVO.js} +392 -421
  48. package/dist/chunk-XICA4PVO.js.map +1 -0
  49. package/dist/{chunk-HNS7UHCM.js → chunk-XK5LZKBD.js} +1 -1
  50. package/dist/chunk-XK5LZKBD.js.map +1 -0
  51. package/dist/cms/index.cjs +146 -0
  52. package/dist/cms/index.cjs.map +1 -0
  53. package/dist/cms/index.d.cts +51 -0
  54. package/dist/cms/index.d.ts +51 -0
  55. package/dist/cms/index.js +146 -0
  56. package/dist/cms/index.js.map +1 -0
  57. package/dist/{export-CEXEzqBf.d.cts → export-BJE8T0YA.d.cts} +279 -191
  58. package/dist/{export-DIkoK8-r.d.ts → export-Con3UKkm.d.ts} +279 -191
  59. package/dist/image-editor/index.cjs +1848 -1218
  60. package/dist/image-editor/index.cjs.map +1 -1
  61. package/dist/image-editor/index.d.cts +85 -93
  62. package/dist/image-editor/index.d.ts +85 -93
  63. package/dist/image-editor/index.js +871 -241
  64. package/dist/image-editor/index.js.map +1 -1
  65. package/dist/opentype-BZEOSOX3.d.ts +188 -0
  66. package/dist/opentype-Dpe75vL5.d.cts +188 -0
  67. package/dist/paint/index.cjs +4 -4
  68. package/dist/paint/index.d.cts +2 -2
  69. package/dist/paint/index.d.ts +2 -2
  70. package/dist/paint/index.js +3 -3
  71. package/dist/paint-editor/index.cjs +147 -145
  72. package/dist/paint-editor/index.cjs.map +1 -1
  73. package/dist/paint-editor/index.d.cts +9 -8
  74. package/dist/paint-editor/index.d.ts +9 -8
  75. package/dist/paint-editor/index.js +7 -5
  76. package/dist/paint-editor/index.js.map +1 -1
  77. package/dist/raster/index.cjs +53 -3
  78. package/dist/raster/index.cjs.map +1 -1
  79. package/dist/raster/index.d.cts +62 -91
  80. package/dist/raster/index.d.ts +62 -91
  81. package/dist/raster/index.js +59 -9
  82. package/dist/{render-BEriFuqK.d.cts → render-B1K_OoLy.d.cts} +87 -2
  83. package/dist/{render-tjzKaik-.d.ts → render-CrmHOgpo.d.ts} +87 -2
  84. package/dist/{retouch-CEIKR22W.d.cts → retouch-Bxkuc0KH.d.cts} +1 -1
  85. package/dist/{retouch-BXmeSCaB.d.ts → retouch-Do6tBs2H.d.ts} +1 -1
  86. package/dist/{surface-C8RRg_Bh.d.cts → surface-Cn30xIV6.d.cts} +6 -1
  87. package/dist/{surface-C8RRg_Bh.d.ts → surface-Cn30xIV6.d.ts} +6 -1
  88. package/dist/{svg-D_cfkhlB.d.cts → svg-BnRj0q8i.d.cts} +16 -4
  89. package/dist/{svg-wIaKLrU0.d.ts → svg-CmmBFfGT.d.ts} +16 -4
  90. package/dist/{types-DtL_qLFz.d.ts → types-1SUQKDAw.d.ts} +273 -4
  91. package/dist/{types-DyQq-WPY.d.cts → types-yINF17Ta.d.cts} +273 -4
  92. package/dist/vector/index.cjs +12170 -7343
  93. package/dist/vector/index.cjs.map +1 -1
  94. package/dist/vector/index.d.cts +1579 -16
  95. package/dist/vector/index.d.ts +1579 -16
  96. package/dist/vector/index.js +6303 -1476
  97. package/dist/vector/index.js.map +1 -1
  98. package/dist/warp-B39VsOtd.d.cts +348 -0
  99. package/dist/warp-DMN30aY6.d.ts +348 -0
  100. package/dist/zlib-dxQdgHyN.d.cts +95 -0
  101. package/dist/zlib-dxQdgHyN.d.ts +95 -0
  102. package/package.json +9 -2
  103. package/skills/pixerate-image-editor/SKILL.md +13 -36
  104. package/skills/pixerate-image-editor/references/color-management.md +42 -0
  105. package/skills/pixerate-image-editor/references/photoshop-files.md +41 -0
  106. package/skills/pixerate-paint-editor/SKILL.md +136 -4
  107. package/skills/pixerate-vector/SKILL.md +72 -2
  108. package/dist/chunk-BLWCAQCW.js.map +0 -1
  109. package/dist/chunk-CZOXTEMN.js.map +0 -1
  110. package/dist/chunk-HNS7UHCM.js.map +0 -1
  111. package/dist/chunk-JLYX4V57.cjs +0 -2001
  112. package/dist/chunk-JLYX4V57.cjs.map +0 -1
  113. package/dist/chunk-KB2YBCVS.js +0 -2001
  114. package/dist/chunk-KB2YBCVS.js.map +0 -1
  115. package/dist/chunk-LQEEDHOP.js.map +0 -1
  116. package/dist/chunk-POGBUYWE.cjs.map +0 -1
  117. package/dist/chunk-W3GXSUSQ.cjs.map +0 -1
  118. package/dist/chunk-XIOMMGPD.cjs.map +0 -1
  119. package/dist/chunk-YGIMK4NX.cjs.map +0 -1
  120. package/dist/homography-CZ-tFjay.d.cts +0 -50
  121. package/dist/homography-Ctfjoc1b.d.ts +0 -50
  122. package/dist/warp-CWHZzE3x.d.ts +0 -118
  123. package/dist/warp-D5jz_tN7.d.cts +0 -118
  124. /package/dist/{chunk-LPYOIVLA.js.map → chunk-LKCYANMW.js.map} +0 -0
  125. /package/dist/{chunk-Z4BC6M74.js.map → chunk-UBIXVXPR.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/histogram.ts","../../src/image-editor/selection/overlay.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/layers/text-shapers.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/retouch-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/transform-tools.ts","../../src/image-editor/smart-tools.ts","../../src/image-editor/shape-tools.ts","../../src/image-editor/type-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/shape-type-ui.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","pick","paint","data","out","pct","WRITE_IDEMPOTENT","LAYER_ID","opt","BRUSH","TARGET","summary","id","DEFS","GEOMETRY","spec","mod","bestD","q","partner"],"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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;AChlBO,IAAM,eAAA,EAAiB,GAAA;AAgCvB,SAAS,gBAAA,CAAiB,SAAA,EAAwC;AACvE,EAAA,MAAM,IAAA,EAAM,IAAI,WAAA,CAAY,cAAc,CAAA;AAC1C,EAAA,MAAM,MAAA,EAAQ,IAAI,WAAA,CAAY,cAAc,CAAA;AAC5C,EAAA,MAAM,KAAA,EAAO,IAAI,WAAA,CAAY,cAAc,CAAA;AAC3C,EAAA,MAAM,UAAA,EAAY,IAAI,WAAA,CAAY,cAAc,CAAA;AAChD,EAAA,MAAM,QAAA,EAAoC,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAClD,EAAA,MAAM,WAAA,EAAuC,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AACrD,EAAA,IAAI,QAAA,EAAU,CAAA;AACd,EAAA,IAAI,YAAA,EAAc,CAAA;AAClB,EAAA,MAAM,KAAA,EAAO,SAAA,CAAU,IAAA;AACvB,EAAA,MAAM,IAAA,EAAM,IAAA,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ACJ,IAAA;AACM,IAAA;AACC,MAAA;AACL,MAAA;AACF,MAAA;AACK,QAAA;AACL,QAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AACoB,EAAA;AACtB;AAagB;AACI,EAAA;AACC,EAAA;AACH,EAAA;AACI,EAAA;AACT,IAAA;AACI,IAAA;AACA,IAAA;AACK,IAAA;AACpB,EAAA;AACO,EAAA;AACT;AFynBuB;AACA;AGxqBO;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;AACD,IAAA;AACf,EAAA;AACW,EAAA;AACP,EAAA;AACG,EAAA;AACT;AAEM;AAKe;AAGA;AACJ,EAAA;AACJ,EAAA;AACA,EAAA;AACK,IAAA;AACC,IAAA;AACE,IAAA;AACnB,EAAA;AACO,EAAA;AACT;AAQgB;AACC,EAAA;AACD,EAAA;AACK,IAAA;AACD,IAAA;AACX,IAAA;AACP,EAAA;AACO,EAAA;AACa,IAAA;AACF,IAAA;AAClB,EAAA;AACF;AAG6B;AACN,EAAA;AACA,EAAA;AACN,EAAA;AACjB;AAQgB;AAOK,EAAA;AAC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L,UAAA;AACP,QAAA;AACF,MAAA;AAEY,MAAA;AACE,QAAA;AACE,QAAA;AAChB,MAAA;AAGY,MAAA;AACC,QAAA;AACb,MAAA;AAEY,MAAA;AACd,IAAA;AAGgB,IAAA;AACL,IAAA;AACA,MAAA;AACL,MAAA;AACF,QAAA;AACc,QAAA;AACL,QAAA;AACA,QAAA;AACC,QAAA;AACK,QAAA;AACH,QAAA;AACN,MAAA;AAER,MAAA;AACY,MAAA;AACd,IAAA;AAGY,IAAA;AACA,MAAA;AACD,MAAA;AACL,MAAA;AACF,QAAA;AACW,QAAA;AACG,QAAA;AACC,QAAA;AACX,QAAA;AACA,QAAA;AACO,QAAA;AACP,QAAA;AACA,QAAA;AACO,QAAA;AACL,MAAA;AAER,MAAA;AACY,MAAA;AACd,IAAA;AAGY,IAAA;AACA,MAAA;AACD,MAAA;AACL,MAAA;AACW,QAAA;AACC,QAAA;AACR,UAAA;AACS,UAAA;AACT,UAAA;AACN,QAAA;AACa,QAAA;AACC,UAAA;AACR,UAAA;AACA,UAAA;AACA,UAAA;AACO,UAAA;AACP,UAAA;AACN,QAAA;AACI,QAAA;AACO,QAAA;AACH,UAAA;AACG,UAAA;AACX,QAAA;AACa,QAAA;AACA,UAAA;AACP,UAAA;AACS,UAAA;AACA,UAAA;AACT,UAAA;AACA,UAAA;AACO,UAAA;AACP,UAAA;AACA,UAAA;AACO,UAAA;AACb,QAAA;AACM,MAAA;AAER,MAAA;AACY,MAAA;AACd,IAAA;AAGY,IAAA;AACA,MAAA;AACD,MAAA;AACL,MAAA;AACW,QAAA;AACC,QAAA;AACF,QAAA;AACC,UAAA;AACC,YAAA;AACC,YAAA;AACP,YAAA;AACN,UAAA;AACW,UAAA;AACC,YAAA;AACA,YAAA;AACC,YAAA;AACA,YAAA;AACb,UAAA;AACK,QAAA;AACC,UAAA;AACE,UAAA;AACJ,UAAA;AACN,QAAA;AACI,QAAA;AACA,QAAA;AACO,QAAA;AACP,QAAA;AACA,QAAA;AACO,QAAA;AACA,QAAA;AACL,UAAA;AACA,UAAA;AACS,UAAA;AACT,UAAA;AACN,QAAA;AACW,QAAA;A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AACP,IAAA;AACX,EAAA;AACO,EAAA;AACT;AAMyB;AACb,EAAA;AACM,EAAA;AACC,EAAA;AACG,EAAA;AACJ,IAAA;AACK,IAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACD,IAAA;AACA,IAAA;AACD,IAAA;AACE,IAAA;AACb,IAAA;AACU,IAAA;AACK,IAAA;AACH,IAAA;AACT,IAAA;AACW,IAAA;AACA,IAAA;AACT,IAAA;AACX,EAAA;AACO,EAAA;AACT;ANs9DuB;AACA;AOnvEV;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;AACD,EAAA;AACH,EAAA;AACI,EAAA;AACb,EAAA;AACT;AAM0B;AACV,EAAA;AACA,EAAA;AACM,EAAA;AACN,EAAA;AACG,EAAA;AACE,IAAA;AACD,MAAA;AACA,MAAA;AACG,MAAA;AACnB,IAAA;AACF,EAAA;AACW,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACI,EAAA;AACb,EAAA;AACT;AAOgB;AAOJ,EAAA;AACyB,EAAA;AACf,EAAA;AACJ,IAAA;AACG,IAAA;AACP,IAAA;AACM,IAAA;AACE,MAAA;AACA,QAAA;AACD,QAAA;AAEV,QAAA;AAIO,QAAA;AACE,UAAA;AACJ,UAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACS,IAAA;AACM,MAAA;AACH,MAAA;AACA,MAAA;AACO,MAAA;AACD,MAAA;AACD,QAAA;AACD,QAAA;AACC,QAAA;AACA,UAAA;AACD,UAAA;AACZ,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACW,EAAA;AACI,EAAA;AACI,IAAA;AACA,IAAA;AACD,IAAA;AAClB,EAAA;AACoB,EAAA;AACA,EAAA;AACA,IAAA;AACF,IAAA;AAClB,EAAA;AACe,EAAA;AACI,EAAA;AACZ,EAAA;AACT;AAG4B;AACL,EAAA;AACP,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACG,EAAA;AACF,IAAA;AACA,MAAA;AACE,MAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AAChB,IAAA;AACF,EAAA;AACe,EAAA;AACjB;AAOgB;AACJ,EAAA;AACK,EAAA;AACG,IAAA;AAClB,EAAA;AACkB,EAAA;AACL,EAAA;AAEO,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACJ,EAAA;AACA,EAAA;AACF,EAAA;AACM,EAAA;AACA,IAAA;AACF,IAAA;AAClB,EAAA;AACc,EAAA;AACE,EAAA;AACA,EAAA;AACI,EAAA;AACA,IAAA;AACF,IAAA;AAClB,EAAA;AACO,EAAA;AACT;APyrEuB;AACA;AQz+EP;AAGc;AACX,EAAA;AACC,EAAA;AACX,EAAA;AACT;AAGwC;AACvB,EAAA;AACjB;AAcgB;AAEJ,EAAA;AAEK,IAAA;AACA,IAAA;AACC,MAAA;AACA,MAAA;AACC,MAAA;AACJ,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACkB,EAAA;AACA,EAAA;AACD,EAAA;AACF,EAAA;AACC,EAAA;AACH,EAAA;AACC,EAAA;AACF,EAAA;AACe,EAAA;AAChB,IAAA;AACA,IAAA;AACb,EAAA;AACqB,EAAA;AACF,IAAA;AACC,IAAA;AACR,IAAA;AACE,IAAA;AACA,MAAA;AACE,MAAA;AACK,MAAA;AACnB,IAAA;AACO,IAAA;AACT,EAAA;AACO,EAAA;AACD,IAAA;AACU,IAAA;AACD,MAAA;AACM,MAAA;AACnB,IAAA;AACc,IAAA;AACA,MAAA;AACI,MAAA;AACR,MAAA;AACK,QAAA;AACP,QAAA;AACU,QAAA;AAEC,QAAA;AACT,QAAA;AACQ,QAAA;AACF,QAAA;AACd,MAAA;AACO,MAAA;AACT,IAAA;AACc,IAAA;AACG,MAAA;AACA,MAAA;AACP,MAAA;AACK,QAAA;AACL,QAAA;AACO,QAAA;AACF,QAAA;AACA,QAAA;AACb,MAAA;AACO,MAAA;AACT,IAAA;AACmB,IAAA;AACH,MAAA;AACP,MAAA;AACA,MAAA;AACS,MAAA;AAClB,IAAA;AACW,IAAA;AACG,MAAA;AACE,MAAA;AACD,MAAA;AACC,MAAA;AAChB,IAAA;AACF,EAAA;AACF;AAqCgB;AACK,EAAA;AACH,EAAA;AACc,IAAA;AACZ,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACE,IAAA;AACF,IAAA;AAClB,EAAA;AACoB,EAAA;AACP,EAAA;AACH,IAAA;AACO,MAAA;AACH,MAAA;AACb,IAAA;AACiB,IAAA;AACE,IAAA;AACD,IAAA;AACF,IAAA;AACE,IAA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AA;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,EA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EAAA;AACL,IAAA;AACE,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAGgB;AACK,EAAA;AACK,EAAA;AACd,EAAA;AACU,EAAA;AACD,IAAA;AACD,IAAA;AACE,IAAA;AACpB,EAAA;AACgB,EAAA;AACT,EAAA;AACT;AAGgB;AACuB,EAAA;AACrB,EAAA;AACH,IAAA;AACQ,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AASa;AACX,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACA,MAAA;AACG,MAAA;AACX,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MA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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,MAAA;AACE,MAAA;AACF,MAAA;AACP,MAAA;AACA,QAAA;AACO,QAAA;AACC,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AACW,IAAA;AACX,IAAA;AACF,EAAA;AACO,EAAA;AACC,IAAA;AACO,IAAA;AACM,IAAA;AACA,IAAA;AACnB,IAAA;AACF,EAAA;AACM,EAAA;AACS,IAAA;AACN,IAAA;AACL,MAAA;AACQ,QAAA;AACQ,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACQ,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACM,QAAA;AACF,UAAA;AACK,UAAA;AACL,UAAA;AACC,UAAA;AACA,UAAA;AACD,UAAA;AACC,UAAA;AACX,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAEkB;AAGT;AACM,EAAA;AACN,EAAA;AACC,IAAA;AACO,IAAA;AAGD,IAAA;AACO,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACA,MAAA;AACC,MAAA;AACJ,MAAA;AACI,MAAA;AACD,MAAA;AACD,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACC,MAAA;AACD,MAAA;AACE,MAAA;A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MAAA;AACN,MAAA;AACX,IAAA;AACF,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACE,IAAA;AACA,IAAA;AACH,IAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACX,IAAA;AACF,EAAA;AAE8B,EAAA;AACtB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACH,IAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACX,IAAA;AACF,EAAA;AAE8B,EAAA;AACtB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACH,IAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACX,IAAA;AACF,EAAA;AAE2B,EAAA;AACnB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACDA,QAAAA;AACK,QAAA;AACH,QAAA;AACA,QAAA;AACA,QAAA;AACD,QAAA;AACZ,MAAA;AACW,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACI,IAAA;AACN,MAAA;AAC6B,MAAA;AACvB,MAAA;AACJ,MAAA;AACJ,MAAA;AACX,IAAA;AACF,EAAA;AAEkB,EAAA;AACpB;AxBokUuB;AACA;AyBx9UR;AACC;AACVD;AACgB;AAEH;AACK;AACb;AACH,EAAA;AACY,EAAA;AACL,EAAA;AACf;AAEa;AACE;AACP,EAAA;AACS,EAAA;AACL,EAAA;AACA,EAAA;AACG,EAAA;AACf;AACY;AACH,EAAA;AACA,EAAA;AACI,EAAA;AACC,EAAA;AACO,EAAA;AACrB;AAGa;AACH,EAAA;AACD,EAAA;AACC,IAAA;AACO,IAAA;AACI,IAAA;AACE,IAAA;AACnB,IAAA;AACF,EAAA;AACW,EAAA;AACH,IAAA;AACO,IAAA;AACD,IAAA;AACC,MAAA;AACC,MAAA;AACC,MAAA;AACf,IAAA;AACW,IAAA;AACX,IAAA;AACF,EAAA;AACO,EAAA;AAEH,IAAA;AACK,IAAA;AACL,MAAA;AACQ,QAAA;AACO,QAAA;AACD,QAAA;AACF,UAAA;AACL,UAAA;AACO,UAAA;AACD,UAAA;AACX,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACQ,MAAA;AACR,MAAA;AACQ,QAAA;AACO,QAAA;AACC,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACO,QAAA;AACD,QAAA;AACF,UAAA;AACL,UAAA;AACO,UAAA;AACE,UAAA;AACd,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACO,QAAA;AACD,QAAA;AACF,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACE,UAAA;AACE,UAAA;AACA,UAAA;AACC,UAAA;AACf,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACU,EAAA;AACK,IAAA;AACN,IAAA;AACG,MAAA;AACR,MAAA;AACQ,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACD,YAAA;AACD,cAAA;AACN,cAAA;AACA,cAAA;AACO,cAAA;AACC,gBAAA;AACN,gBAAA;AACA,gBAAA;AACK,kBAAA;AACA,kBAAA;AACC,kBAAA;AACC,kBAAA;AACL,kBAAA;AACF,gBAAA;AACA,gBAAA;AACA,gBAAA;AACF,cAAA;AACF,YAAA;AACU,YAAA;AACZ,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACU,EAAA;AACF,IAAA;AACO,IAAA;AACD,IAAA;AACH,MAAA;AACC,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACD,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACc,MAAA;AACN,QAAA;AACO,QAAA;AACN,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACD,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACe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B,EAAA;AACO,EAAA;AACT;AAMyB;AACR,EAAA;AACG,EAAA;AACpB;AAG8B;AACb,EAAA;AACA,EAAA;AACK,EAAA;AACP,EAAA;AAEG,EAAA;AAClB;AAkC0B;AACH,EAAA;AACN,EAAA;AACM,EAAA;AACJ,EAAA;AACG,EAAA;AACL,EAAA;AACC,EAAA;AACT,EAAA;AACK,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;AACZ,EAAA;AACF;AAGsB;AACL,EAAA;AACI,EAAA;AACf,EAAA;AACc,IAAA;AACT,MAAA;AACQ,QAAA;AACX,QAAA;AACG,MAAA;AACQ,QAAA;AACX,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;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACG,QAAA;AACN,QAAA;AACG,MAAA;AACG,QAAA;AACN,QAAA;AACG,MAAA;AACA,MAAA;AACQ,QAAA;AACX,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACO,QAAA;AACV,QAAA;AACG,MAAA;AACO,QAAA;AACV,QAAA;AACG,MAAA;AACW,QAAA;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,I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IAAA;AACT,IAAA;AACA,IAAA;AACC,IAAA;AACK,MAAA;AACA,MAAA;AACb,MAAA;AACF,IAAA;AACU,IAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AACI,MAAA;AACH,MAAA;AACA,MAAA;AACd,IAAA;AACe,IAAA;AACD,IAAA;AACF,IAAA;AACf,EAAA;AAAA;AAGqC,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;AnCmyYuB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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 * 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, type ShapeLayer, type TypeLayer } 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): boolean =>\n l.type === 'annotation'\n ? l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask\n : (l.type === 'shape' || l.type === 'type') && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation!.blend ?? 'normal') && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;\n\nconst overlayViews = new WeakMap<ImageLayer, AnnotationLayer>();\n\n/** An annotation layer, or the annotation view of a shape layer migrated from one (memoised). */\nfunction overlayView(l: ImageLayer): AnnotationLayer {\n if (l.type === 'annotation') return l;\n let view = overlayViews.get(l);\n if (!view) {\n const shape = l as ShapeLayer | TypeLayer;\n view = { type: 'annotation', id: shape.id, name: shape.name, visible: shape.visible, opacity: 1, blend: 'normal', annotation: shape.annotation! };\n overlayViews.set(l, view);\n }\n return view;\n}\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 and shape and type layers that still are the annotation they were\n * migrated from (given as annotation views; 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.type === 'shape' || l.type === 'type') && l.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).filter((l) => l.visible).map(overlayView),\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 /**\n * On-canvas text editing (`TextEditSession.overlay()`): the caret as a segment, the selected\n * ranges as quads and the text's frame, in natural pixels.\n */\n textEdit?: { caret: [ImagePoint, ImagePoint] | null; selection: ImagePoint[][]; frame: ImagePoint[] | null } | 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 // Text editing: the frame (dashed), the selection (tinted) and the caret, a fixed width on screen.\n if (options.textEdit) {\n const t = options.textEdit;\n ctx.save();\n try {\n const scr = (p: ImagePoint) => projectImagePoint(p, projection);\n const poly = (pts: ImagePoint[]) => {\n ctx.beginPath();\n pts.map(scr).forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));\n ctx.closePath();\n };\n if (t.frame) {\n poly(t.frame);\n ctx.setLineDash([4, 3]);\n ctx.lineWidth = 1;\n ctx.strokeStyle = 'rgba(99,102,241,0.9)';\n ctx.stroke();\n ctx.setLineDash([]);\n }\n ctx.fillStyle = 'rgba(99,102,241,0.35)';\n for (const q of t.selection) {\n poly(q);\n ctx.fill();\n }\n if (t.caret) {\n const [a, b] = t.caret.map(scr);\n ctx.beginPath();\n ctx.moveTo(a.x, a.y);\n ctx.lineTo(b.x, b.y);\n ctx.lineWidth = 3;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n ctx.lineWidth = 1.5;\n ctx.strokeStyle = '#111827';\n ctx.stroke();\n }\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 * Text shapers for type layers (see `TextShaper` in `text.ts`):\n *\n * - `createCanvasTextShaper`: the browser's own fonts and text drawing through Canvas 2D\n * (`measureText`, `fillText`). The default in browsers. It cannot give outlines.\n * - `createOutlineTextShaper`: glyph outlines from font objects the host loads, typed after\n * opentype.js's `Font` (`getPath`, `getAdvanceWidth`, `unitsPerEm`, …) without depending on it.\n * Pure: it renders anywhere (servers, workers, tests) and makes Convert to Shape possible.\n */\n\nimport type { BezierPath } from '../../canvas/pathops/bezier';\nimport type { TextFont, TextShaper } from './text';\n\nconst GENERIC = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'math', 'emoji', 'fangsong']);\n\n/** A family as it goes into a CSS `font` value: generics and lists as written, other names quoted when needed. */\nexport function cssFontFamily(family: string): string {\n const f = family.trim();\n if (GENERIC.has(f.toLowerCase()) || f.includes(',') || /^[\"']/.test(f)) return f;\n return /^[A-Za-z_-][\\w -]*$/.test(f) && !/\\s{2}/.test(f) ? f : `\"${f.replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/** The CSS `font` shorthand for a font (`italic 700 24px Inter`). */\nexport function cssFont(font: TextFont, family = font.family): string {\n return `${font.italic ? 'italic ' : ''}${font.weight} ${font.size}px ${cssFontFamily(family)}`;\n}\n\nexport interface CanvasTextShaperOptions {\n /** A 1 × 1 canvas for measuring (default an `OffscreenCanvas`, else a DOM canvas). */\n createCanvas?(width: number, height: number): HTMLCanvasElement | OffscreenCanvas;\n /** Family used when one is missing (default `sans-serif`). */\n fallbackFamily?: string;\n}\n\n/**\n * A shaper over Canvas 2D. A family is missing when text set in it measures the same as in\n * both `monospace` and `serif` (the browser fell back), so web fonts must be loaded before use.\n * Returns `null` where there is no 2D canvas.\n */\nexport function createCanvasTextShaper(options: CanvasTextShaperOptions = {}): TextShaper | null {\n const make =\n options.createCanvas ??\n ((w: number, h: number) => {\n if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h);\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = w;\n c.height = h;\n return c;\n }\n return null as unknown as HTMLCanvasElement;\n });\n const canvas = make(1, 1);\n const ctx = canvas?.getContext('2d') as CanvasRenderingContext2D | null;\n if (!ctx || typeof ctx.measureText !== 'function') return null;\n const fallback = options.fallbackFamily ?? 'sans-serif';\n const available = new Map<string, boolean>();\n const widths = new Map<string, number[]>();\n const metrics = new Map<string, { ascent: number; descent: number }>();\n const probe = 'mmmmmmmmmmlli1WQ@#';\n const width = (font: string, text: string) => {\n ctx.font = font;\n return ctx.measureText(text).width;\n };\n const isAvailable = (family: string) => {\n const f = family.trim();\n if (GENERIC.has(f.toLowerCase()) || f.includes(',')) return true;\n let hit = available.get(f);\n if (hit === undefined) {\n const q = cssFontFamily(f);\n hit = width(`72px ${q}, monospace`, probe) !== width('72px monospace', probe) || width(`72px ${q}, serif`, probe) !== width('72px serif', probe);\n available.set(f, hit);\n }\n return hit;\n };\n return {\n id: 'canvas',\n resolve(font) {\n const ok = isAvailable(font.family);\n return { family: ok ? font.family : fallback, available: ok };\n },\n metrics(font) {\n const key = cssFont(font);\n let m = metrics.get(key);\n if (!m) {\n ctx.font = key;\n ctx.textBaseline = 'alphabetic';\n const t = ctx.measureText('Hg') as TextMetrics & { emHeightAscent?: number; emHeightDescent?: number };\n // The em box, as `textBaseline: 'top'` places text (what text annotations used).\n const ascent = t.emHeightAscent ?? t.fontBoundingBoxAscent ?? font.size * 0.8;\n const descent = t.emHeightDescent ?? t.fontBoundingBoxDescent ?? font.size * 0.2;\n m = { ascent: Number.isFinite(ascent) ? ascent : font.size * 0.8, descent: Number.isFinite(descent) ? descent : font.size * 0.2 };\n metrics.set(key, m);\n }\n return m;\n },\n measure(text, font) {\n const key = `${cssFont(font)}|${text}`;\n let w = widths.get(key);\n if (!w) {\n ctx.font = cssFont(font);\n w = [0];\n for (let i = 1; i <= text.length; i++) w.push(ctx.measureText(text.slice(0, i)).width);\n if (widths.size > 2000) widths.clear();\n widths.set(key, w);\n }\n return w;\n },\n draw(target, text, font, x, y) {\n target.font = cssFont(font);\n target.textBaseline = 'alphabetic';\n target.textAlign = 'left';\n target.fillText(text, x, y);\n },\n families() {\n const out = new Set<string>(['sans-serif', 'serif', 'monospace']);\n const fonts = typeof document !== 'undefined' ? (document as Document & { fonts?: Iterable<FontFace> }).fonts : undefined;\n if (fonts && typeof (fonts as unknown as { forEach?: unknown }).forEach === 'function') (fonts as unknown as { forEach(cb: (f: FontFace) => void): void }).forEach((f) => out.add(f.family.replace(/^[\"']|[\"']$/g, '')));\n return [...out];\n },\n };\n}\n\n/** A path command as opentype.js returns it. */\nexport interface OutlinePathCommand {\n type: 'M' | 'L' | 'C' | 'Q' | 'Z';\n x?: number;\n y?: number;\n x1?: number;\n y1?: number;\n x2?: number;\n y2?: number;\n}\n\n/** The parts of an opentype.js `Font` the outline shaper uses. */\nexport interface OutlineFontFace {\n unitsPerEm: number;\n ascender: number;\n descender: number;\n getAdvanceWidth(text: string, fontSize: number, options?: { kerning?: boolean }): number;\n getPath(text: string, x: number, y: number, fontSize: number, options?: { kerning?: boolean }): { commands: OutlinePathCommand[] };\n names?: { fontFamily?: Record<string, string>; postScriptName?: Record<string, string> };\n tables?: { os2?: { usWeightClass?: number; fsSelection?: number } };\n}\n\n/** A face with its family, weight and style (read from the font's names when not given). */\nexport interface OutlineFontEntry {\n face: OutlineFontFace;\n family?: string;\n weight?: number;\n italic?: boolean;\n}\n\n/**\n * A shaper over outline fonts the host loads (opentype.js `Font` objects or anything with the same\n * methods). Fonts are matched by family (case-insensitive), then the nearest weight and the\n * italic flag; a missing family uses `fallback` (default the first font) and is reported missing.\n */\nexport function createOutlineTextShaper(fonts: ReadonlyArray<OutlineFontFace | OutlineFontEntry>, options: { fallback?: string; id?: string } = {}): TextShaper {\n if (!fonts.length) throw new Error('createOutlineTextShaper needs at least one font.');\n const entries = fonts.map((f) => {\n const e: OutlineFontEntry = 'face' in f ? f : { face: f };\n const names = e.face.names;\n const family = e.family ?? names?.fontFamily?.en ?? Object.values(names?.fontFamily ?? {})[0] ?? 'Font';\n const weight = e.weight ?? e.face.tables?.os2?.usWeightClass ?? 400;\n const italic = e.italic ?? Boolean((e.face.tables?.os2?.fsSelection ?? 0) & 1);\n const postScript = names?.postScriptName?.en ?? Object.values(names?.postScriptName ?? {})[0];\n return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };\n });\n const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();\n const pick = (font: TextFont) => {\n if (font.postScriptName) {\n const exact = entries.find((e) => e.postScript === font.postScriptName);\n if (exact) return { entry: exact, available: true };\n }\n const key = font.family.trim().toLowerCase();\n let pool = entries.filter((e) => e.key === key);\n const available = pool.length > 0;\n if (!available) pool = entries.filter((e) => e.key === fallbackKey);\n if (!pool.length) pool = entries;\n const styled = pool.filter((e) => e.italic === font.italic);\n const list = styled.length ? styled : pool;\n const entry = list.reduce((best, e) => (Math.abs(e.weight - font.weight) < Math.abs(best.weight - font.weight) ? e : best));\n return { entry, available };\n };\n const toPath = (cmds: OutlinePathCommand[]): BezierPath => {\n const out: BezierPath = [];\n for (const c of cmds) {\n if (c.type === 'M' || c.type === 'L') out.push({ type: c.type, x: c.x!, y: c.y! });\n else if (c.type === 'C') out.push({ type: 'C', x1: c.x1!, y1: c.y1!, x2: c.x2!, y2: c.y2!, x: c.x!, y: c.y! });\n else if (c.type === 'Q') out.push({ type: 'Q', x1: c.x1!, y1: c.y1!, x: c.x!, y: c.y! });\n else out.push({ type: 'Z' });\n }\n return out;\n };\n return {\n id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? 'i' : ''}`).join(',')}`,\n resolve(font) {\n const { entry, available } = pick(font);\n return { family: entry.family, available };\n },\n metrics(font) {\n const { face } = pick(font).entry;\n const k = font.size / face.unitsPerEm;\n return { ascent: face.ascender * k, descent: -face.descender * k };\n },\n measure(text, font) {\n const { face } = pick(font).entry;\n const out = [0];\n for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));\n return out;\n },\n outline(text, font, x, y) {\n return toPath(pick(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);\n },\n families() {\n return [...new Set(entries.map((e) => e.family))];\n },\n };\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 { applyHomography, homographyFromAffine, multiplyHomographies, type Homography } from '../geom/homography';\nimport { invertMatrix as invertAffine } from '../geom/affine';\nimport { findLayer, insertLayer, mapLayer, newLayerId, type ImageLayer, type LayerTarget, type PixelLayer, type RasterMask, type ShapeLayer, type SmartObjectLayer } from './layers/model';\nimport { renderShape, shapeGeometryBounds, transformShapeComponents, type ShapeEnv } from './layers/shape';\nimport { rerenderText, textMatrix, typeFrame, type TextEnv } from './layers/text';\nimport type { TypeLayer } 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 matrixFromCorners,\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 /** What shape layers render with (canvas size, document patterns). */\n shapeEnv(): ShapeEnv;\n /** What type layers lay out and draw with. */\n textEnv(): TextEnv;\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 /** True when the layer is a shape layer (the transform maps its paths and renders them again). */\n readonly shapeLayer: boolean = false;\n private readonly shape: ShapeLayer | null = null;\n /** True when the layer is a type layer (the transform sets its text matrix; affine only). */\n readonly typeLayer: boolean = false;\n private readonly typeTarget: TypeLayer | null = null;\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' || target?.type === 'shape' || target?.type === 'type') && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : 'pixels')) !== 'mask';\n // A smart object or a shape 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' && layer.type !== 'shape' && layer.type !== 'type') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, 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 (layer.type === 'type' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n // As a smart object: the frame is the text's own box, the corners where it lands now.\n const frame = typeFrame(layer, host.textEnv().shaper);\n const m = textMatrix(layer);\n const at = (x: number, y: number) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });\n this.typeTarget = layer;\n this.typeLayer = true;\n this.box = frame;\n this.startGeometry = {\n box: frame,\n corners: [at(frame.x, frame.y), at(frame.x + frame.width, frame.y), at(frame.x + frame.width, frame.y + frame.height), at(frame.x, frame.y + frame.height)],\n warp: null,\n };\n this.geometryValue = this.startGeometry;\n this.startInterp = this.interp;\n this.moves = mask && linked ? ['pixels', 'mask'] : ['pixels'];\n if (mask && linked) this.parts.push({ what: 'mask', offset: o, moving: mask.surface, base: null, reduced: new Map() });\n this.shown = this.startLayers;\n return;\n }\n if (layer.type === 'shape' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n const g = shapeGeometryBounds(layer);\n if (!g) fail(`Shape layer \"${layer.name || id}\" has no path to transform.`);\n // A straight line has no height (or width): give the frame one pixel so it can turn.\n const w = Math.max(g!.width, 1);\n const h = Math.max(g!.height, 1);\n box = { x: g!.x + (g!.width - w) / 2, y: g!.y + (g!.height - h) / 2, width: w, height: h };\n this.shape = layer;\n this.shapeLayer = true;\n moves.push('pixels');\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 }\n } else 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 if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {\n fail('Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.');\n }\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 if (this.typeTarget) return { layers: this.resampleType(), 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 if (this.shape) layers = this.resampleShape(final, preview, warp ? f : null, h);\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 /**\n * A shape with its paths mapped through the current geometry and rendered again (live shapes\n * stay live under an affine map; perspective and warps turn them into plain paths). Previews\n * render only the visible part.\n */\n private resampleShape(final: boolean, preview: FreeTransformPreview, warpMap: ((p: { x: number; y: number }) => { x: number; y: number } | null) | null, h: Homography | null): ImageLayer[] {\n const layer = this.shape!;\n let components = layer.components;\n if (!isIdentityTransform(this.geometryValue)) {\n const affine = !warpMap && h && Math.abs(h[6]) < 1e-12 && Math.abs(h[7]) < 1e-12 && Math.abs(h[8] - 1) < 1e-12 ? ([h[0], h[3], h[1], h[4], h[2], h[5]] as const) : undefined;\n const map = warpMap ?? ((p: { x: number; y: number }) => (h ? applyHomography(h, p) : p));\n components = transformShapeComponents(layer.components, (p) => map(p) ?? p, affine);\n }\n const moved: ShapeLayer = { ...layer, components };\n delete moved.annotation;\n const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });\n return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));\n }\n\n /**\n * A type layer with its text matrix set from the corners (affine only: scale, rotate, skew and\n * flips), drawn again; a linked mask follows by the change's homography.\n */\n private resampleType(): ImageLayer[] {\n const layer = this.typeTarget!;\n const g = this.geometryValue;\n if (hasWarp(g)) fail('Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.');\n const m = matrixFromCorners(g.box, g.corners);\n if (!m) fail('Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.');\n const next: TypeLayer = { ...layer, transform: m! };\n delete next.annotation;\n let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));\n // A linked mask follows the change of the matrix.\n const change = multiplyHomographies(homographyFromAffine(invertAffine(textMatrix(layer))), homographyFromAffine(m!));\n for (const part of this.parts) {\n if (part.what !== 'mask') continue;\n const surface = transformSurface(part.moving, change, { interpolation: this.interp === 'nearest' ? 'nearest' : 'bilinear' });\n layers = mapLayer(layers, layer.id, (l) => ({ ...l, mask: { ...(l.mask as RasterMask), surface } }) as ImageLayer);\n }\n return layers;\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 && !this.typeTarget) 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' || layer?.type === 'shape' || layer?.type === 'type')) 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 { isBlendMode, 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 type LiveShape,\n type ShapeComponent,\n type ShapeFillRule,\n type ShapeOperation,\n type ShapeLayer,\n type ShapePaint,\n type ShapeStroke,\n type ParagraphStyle,\n type TextStyle,\n type TypeLayer,\n hasSurface,\n} from './layers/model';\nimport {\n annotationOfShape,\n createShapeLayer,\n liveComponent,\n mergeShapeComponents as mergeShapeComponentsOp,\n normalizeLiveShape,\n normalizeShapePaint,\n normalizeShapeStroke,\n normalizeSubpaths,\n offsetShape,\n requireOperation,\n requireShapeLayer,\n rerenderShape,\n shapeAsPixelLayer,\n shapeFromAnnotation,\n shapeGeometryBounds,\n transformShapeComponents,\n updateShapeLayer,\n type ShapeEnv,\n} from './layers/shape';\nimport { bezierToSubpaths, roundSubpaths } from '../vector/geometry';\nimport {\n DEFAULT_TEXT_STYLE,\n annotationOfType,\n caretAt,\n createTypeLayer,\n layoutText,\n normalizeText,\n patchParagraphStyle,\n patchTextStyle,\n replaceTextRange,\n requireTypeLayer,\n restyleRange,\n styleAt,\n textIndexAt,\n textOutlines,\n textMatrix,\n textRange,\n toTextSpace,\n transformTypeLayer,\n typeAsPixelLayer,\n typeLayerName,\n updateTypeLayer,\n type TextEnv,\n type TextLayout,\n type TextShaper,\n} from './layers/text';\nimport { createCanvasTextShaper } from './layers/text-shapers';\nimport type { VectorSubpath } from '../vector/types';\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 annotationOfLayer,\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 encodeSurface,\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 tileCoord,\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 DEFAULT_RETOUCH,\n MixerStroke,\n RegionPixelSource,\n RetouchDabStroke,\n StrokeCoverage,\n TilePixelSource,\n applyRetouchToTile,\n applyStrokeToTile,\n cleanMixer,\n healPixels,\n loadMixer,\n normalizeBrush,\n normalizeMixer,\n normalizeRetouch,\n readSourceRegion,\n rgbaViewOfGrey,\n smudgeEffect,\n spongeEffect,\n spotHealPixels,\n spotHealWindow,\n toneOf,\n type BrushSettings,\n type MixerLoad,\n type MixerSettings,\n type MixerTarget,\n type PaintTileTarget,\n type PixelSource,\n type RetouchApply,\n type RetouchPaint,\n type RetouchSettings,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, encodePng, isPng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport type { Interpolation } from '../raster/transform';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd, prepareNestedLayers, smartContentsFile } from './psd/export';\nimport { BROWSER_IMAGE_TYPES } from './psd/smart';\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, rotateMatrix, 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/** 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 /** The retouching tools' options (aligned, sample, spot healing type, tone range, exposure, sponge mode, smudge strength). */\n retouch: RetouchSettings;\n /** The Clone Stamp and Healing Brush source point (Alt-click), or `null` when none is set. */\n cloneSource: CloneSource | null;\n}\n\n/** Where the Clone Stamp and Healing Brush sample from. */\nexport interface CloneSource {\n /** The source point, document pixels. */\n x: number;\n y: number;\n /** With Aligned on: the offset (source − stroke start) the first stroke fixed, kept for the next strokes; `null` until a stroke has used the point. */\n offset: { x: number; y: number } | null;\n}\n\n/**\n * The geometry of one shape component: a live shape (`{ kind: 'rect' | 'ellipse' | 'polygon' |\n * 'star' | 'line', … }`, see `LiveShape`) or subpaths (`[{ anchors: [{ x, y, in?, out? }],\n * closed }]` or SVG path data), and how it combines with the components below it.\n */\nexport interface ShapeGeometryInput {\n shape?: LiveShape | Record<string, unknown>;\n subpaths?: VectorSubpath[] | string;\n /** Default `combine`. */\n op?: ShapeOperation;\n}\n\n/** A stroke as tools and UIs give it: any `ShapeStroke` fields, the paint as a colour string or a paint (or `color`). */\nexport type ShapeStrokeInput = Omit<Partial<ShapeStroke>, 'paint'> & { paint?: ShapePaint | string; color?: string };\n\n/** A new shape layer: its geometry (one component, or `components`), fill and stroke. */\nexport interface ShapeLayerInit extends ShapeGeometryInput, LayerPlacement {\n /** Several components instead of `shape` / `subpaths`, bottom first. */\n components?: ShapeGeometryInput[];\n name?: string;\n /** A colour string or a paint; `null` for none. Default the foreground colour (none for a stroke-only open path). */\n fill?: ShapePaint | string | null;\n /** `{ width, paint, align?, cap?, join?, miterLimit?, dashes?, dashOffset? }`, or `null` / missing for none. */\n stroke?: ShapeStrokeInput | null;\n fillRule?: ShapeFillRule;\n opacity?: number;\n blend?: BlendMode;\n}\n\n/** A shape layer as tools and panels read it. */\nexport interface ShapeLayerInfo {\n id: string;\n name: string;\n /** Bottom to top; `index` is what component edits take. */\n components: Array<{ index: number; op: ShapeOperation; live: LiveShape | null; subpaths: VectorSubpath[]; bounds: CropBox | null }>;\n fillRule: ShapeFillRule;\n fill: ShapePaint | null;\n stroke: ShapeStroke | null;\n /** Bounds of the paths (document pixels, not rounded), or `null` when empty. */\n geometryBounds: CropBox | null;\n /** Bounds of the rendered pixels (stroke and antialiasing included). */\n bounds: CropBox | null;\n /** The annotation id while the layer is still the annotation it was migrated from. */\n annotationId?: string;\n}\n\n/** A new type layer: its text, where it goes, and its character and paragraph style. */\nexport interface TextLayerInit extends LayerPlacement {\n text: string;\n /**\n * Document x, y of the text's origin: point text's first baseline at its alignment edge (left\n * edge for left-aligned, centre for centred, right edge for right-aligned); a paragraph box's\n * top-left.\n */\n x: number;\n y: number;\n /** Paragraph (area) text: wrap inside a `width × height` box (pixels). Missing: point text. */\n box?: { width: number; height: number };\n /** Character style of the whole text (default 24 px sans-serif, the foreground colour). */\n style?: Partial<TextStyle>;\n paragraph?: ParagraphStyle;\n /** Degrees clockwise about the origin. */\n rotation?: number;\n name?: string;\n opacity?: number;\n blend?: BlendMode;\n}\n\n/** A type layer as tools and panels read it. Ranges are UTF-16 code units, `end` exclusive. */\nexport interface TypeLayerInfo {\n id: string;\n name: string;\n text: string;\n runs: Array<{ start: number; end: number; style: TextStyle }>;\n paragraphs: Array<{ start: number; end: number; style: ParagraphStyle }>;\n frame: TypeLayer['frame'];\n /** Text space → document. */\n transform: AffineMatrix;\n /** Laid-out lines: text range, baseline (text space) and width; empty without a shaper. */\n lines: Array<{ start: number; end: number; baseline: number; width: number }>;\n /** Rendered bounds in the document. */\n bounds: CropBox | null;\n /** Families drawn with a fallback because they are missing here. */\n missingFonts: string[];\n /** `live`: drawn here from the text; `stored`: pixels kept from a file until the next edit. */\n rendering: 'live' | 'stored';\n annotationId?: string;\n}\n\n/** The retouching tools (see `@pixerate/editor/paint`'s retouch module). */\nexport type RetouchTool = 'clone' | 'heal' | 'spot-heal' | 'smudge' | 'dodge' | 'burn' | 'sponge';\n\nexport const RETOUCH_TOOLS: readonly RetouchTool[] = ['clone', 'heal', 'spot-heal', 'smudge', 'dodge', 'burn', 'sponge'];\n\nexport const isRetouchTool = (tool: unknown): tool is RetouchTool => RETOUCH_TOOLS.includes(tool as RetouchTool);\n\n/** Paint tools a stroke can use. */\nexport type StrokeTool = 'brush' | 'eraser' | 'mixer' | RetouchTool;\n\nconst STROKE_TOOLS: readonly StrokeTool[] = ['brush', 'eraser', 'mixer', ...RETOUCH_TOOLS];\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 /** Overrides of the paint options' retouch settings (the retouching tools). */\n retouch?: Partial<RetouchSettings>;\n /**\n * Clone Stamp / Healing Brush: the source point for this stroke (document pixels). Given, it\n * becomes the clone source (as an Alt-click), so the stroke starts its offset afresh.\n */\n source?: { x: number; y: 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 * Clone Stamp / Healing Brush: the offset used (source − first stroke point, whole pixels) and\n * where the stroke's first point sampled.\n */\n clone?: { offset: { x: number; y: number }; source: { x: number; y: number }; aligned: boolean };\n /** Spot Healing: the offset of the patch it copied, or `null` when it filled smoothly. */\n spotHeal?: { offset: { x: number; y: number } | null };\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/** A stroke that paints dab by dab into live pixels (Mixer Brush, Smudge, Sponge). */\ninterface LiveDabStroke {\n push(points: readonly StrokePoint[]): CropBox[];\n finish(last?: StrokePoint): CropBox[];\n readonly bounds: CropBox | null;\n readonly dabCount: number;\n}\n\n/**\n * How a retouching stroke starts (see `PaintStrokeSession`): either coverage-based (Clone Stamp,\n * Healing Brush, Spot Healing, Dodge, Burn: `coverage` composited with `paint()` over the\n * pre-stroke pixels, optionally replaced by `finalPaint` when the stroke ends) or sequential\n * (`create`: dabs over live pixels, Smudge and Sponge).\n */\nexport type RetouchStrokeInit =\n | {\n kind: 'coverage';\n coverage: StrokeCoverage;\n apply: RetouchApply;\n /** Called with the first point before any dab is shown (the clone offset is fixed here). */\n start(first: StrokePoint): void;\n /** What the coverage lays while the stroke is drawn. */\n paint(): RetouchPaint;\n /** What it lays once the stroke ends, from the whole coverage (healing); `null` keeps `paint()`. */\n finalPaint?(coverage: CoverageMap): RetouchPaint | null;\n /** Extra result fields. */\n result?(): Partial<PaintStrokeResult>;\n }\n | {\n kind: 'dabs';\n /** Makes the stroke over the session's live pixels (RGBA, or an RGBA view of a mask). */\n create(pixels: PaintTileTarget): LiveDabStroke;\n result?(): Partial<PaintStrokeResult>;\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. Retouching strokes take one of the two shapes.\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: LiveDabStroke | null;\n private readonly mixerInit: MixerStrokeInit | null = null;\n private readonly retouch: RetouchStrokeInit | null = null;\n /** Writes a mask's RGBA view back (sequential retouching on a mask). */\n private readonly flushView: (() => void) | null = null;\n private started = false;\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 | RetouchStrokeInit,\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 if ('kind' in stroke && stroke.kind === 'coverage') {\n this.stroke = stroke.coverage;\n this.mixer = null;\n this.retouch = stroke;\n } else if ('kind' in stroke) {\n this.stroke = null;\n this.retouch = stroke;\n if (this.base.channels === 1) {\n const view = rgbaViewOfGrey(this.mixerPixels());\n this.mixer = stroke.create(view.target);\n this.flushView = view.flush;\n } else this.mixer = stroke.create(this.mixerPixels());\n } else {\n this.stroke = null;\n this.mixer = stroke.create(this.mixerPixels());\n this.mixerInit = stroke;\n }\n }\n\n /** The new tile of a coverage stroke over the pre-stroke tile `src`. */\n private compositeTile(src: Uint8ClampedArray, tx: number, ty: number, options: { into?: Uint8ClampedArray; region?: CropBox } = {}, paint?: RetouchPaint): Uint8ClampedArray {\n const r = this.retouch;\n if (r && r.kind === 'coverage') return applyRetouchToTile(this.stroke!.coverage, src, this.base.channels, tx, ty, paint ?? r.paint(), r.apply, options);\n return applyStrokeToTile(this.stroke!.coverage, src, this.base.channels, tx, ty, this.apply, options);\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 if (!this.started) {\n this.started = true;\n if (this.retouch?.kind === 'coverage') this.retouch.start(points[0]);\n }\n this.last = points[points.length - 1];\n const rects = this.mixer ? this.mixer.push(points) : this.stroke!.push(points);\n this.flushView?.();\n this.update(rects);\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 = this.compositeTile(src, tx, ty, 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 const rects = this.mixer ? this.mixer.finish(last) : this.stroke!.finish(last);\n this.flushView?.();\n this.update(rects);\n this.closed = true;\n const ch = this.base.channels;\n const r = this.retouch;\n if (r?.kind === 'coverage' && r.finalPaint) {\n // Healing: redo every touched tile from the pre-stroke pixels with the final paint.\n const final = r.finalPaint(this.stroke!.coverage);\n if (final) {\n for (const key of this.touched) {\n const { tx, ty } = tileCoord(key);\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n this.live.set(key, { data: this.compositeTile(src, tx, ty, {}, final), uniform: null });\n }\n }\n }\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.mixerInit ? this.mixerInit.end((this.mixer as MixerStroke).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 if (r?.result) Object.assign(result, r.result());\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 if (options.textShaper !== undefined) this.textShaperValue = options.textShaper;\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, retouch: { ...DEFAULT_RETOUCH }, cloneSource: null },\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 || !hasSurface(layer)) 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 || !hasSurface(layer)) 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 * A tight base64 PNG data URL of a pixel or smart layer's content, or `null` for an unknown or\n * non-pixel layer, or when the layer is completely transparent.\n */\n public getLayerDataURL(id: string): string | null {\n const layer = this.getLayer(id);\n if (!layer || !hasSurface(layer)) return null;\n const surface = this.surfaceOf(layer);\n const enc = encodeSurface(surface);\n return enc ? enc.url : 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, smart object or shape composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer | SmartObjectLayer | ShapeLayer | TypeLayer): PixelSurface {\n if (layer.type !== 'pixel' || 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) => annotationOfLayer(l)?.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 if (layer.type === 'shape') {\n // A shape moves its paths: x, y is where the top-left of its geometry goes.\n const g = shapeGeometryBounds(layer) ?? { x: 0, y: 0 };\n const dx = move.x !== undefined ? move.x - g.x : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - g.y : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n requireUnlocked(layer, 'position', 'move it');\n let moved = offsetShape(layer, dx, dy, this.shapeEnv());\n const mask = layer.mask;\n if (mask && mask.linked !== false && !mask.source && (Math.round(dx) || Math.round(dy))) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, Math.round(dx), Math.round(dy)) } };\n this.commitLayers(mapLayer(this.state.layers, id, () => moved), undefined, commitToHistory);\n const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };\n return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };\n }\n if (layer.type === 'type') {\n // A type layer moves through its transform: x, y is where its text-space origin goes.\n const m = layer.transform ?? [1, 0, 0, 1, 0, 0];\n const dx = move.x !== undefined ? move.x - m[4] : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - m[5] : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n requireUnlocked(layer, 'position', 'move it');\n const mx = Math.round(dx);\n const my = Math.round(dy);\n let moved: TypeLayer = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };\n const mask = layer.mask;\n if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };\n this.commitLayers(mapLayer(this.state.layers, id, () => moved), undefined, commitToHistory);\n const t = moved.transform ?? [1, 0, 0, 1, 0, 0];\n return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, 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 shapeEnv: () => this.shapeEnv(),\n textEnv: () => this.textEnv(),\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 || session.typeLayer ? (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', pdf: 'application/pdf', ...BROWSER_IMAGE_TYPES };\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 const f = smartContentsFile(c);\n return { name: f.name, mimeType: f.name.endsWith('.png') ? 'image/png' : 'image/vnd.adobe.photoshop', bytes: f.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)) {\n if (l.type === 'smart' && !isContentsDecoded(l.contents) && l.contents.file && BROWSER_IMAGE_TYPES[l.contents.file.type]) pending.set(l.contents.id, l.contents);\n }\n const left: string[] = [];\n for (const [cid, c] of pending) {\n try {\n const url = `data:${BROWSER_IMAGE_TYPES[c.file!.type]};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 (hasSurface(layer)) 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) : hasSurface(layer) ? 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 /**\n * A small PNG data URL preview of a pixel or smart layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px; `null` for layers without pixels.\n */\n public getLayerThumbnailDataURL(id: string, options: { maxSide?: number; mask?: boolean } = {}): string | null {\n const thumb = this.getLayerThumbnail(id, options);\n if (!thumb) return null;\n return pngDataUrl(thumb.width, thumb.height, thumb.data, 4);\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' | 'retouch' | 'cloneSource'>> & {\n brush?: Partial<BrushSettings>;\n mixer?: Partial<MixerSettings>;\n retouch?: Partial<RetouchSettings>;\n }\n ): PaintOptions {\n const cur = this.state.paint;\n const retouch = options.retouch ? normalizeRetouch(options.retouch, cur.retouch) : cur.retouch;\n // Turning Aligned off (or on) starts the offset afresh at the next stroke.\n const cloneSource = cur.cloneSource && retouch.aligned !== cur.retouch.aligned ? { ...cur.cloneSource, offset: null } : cur.cloneSource;\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 retouch,\n cloneSource,\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 /**\n * Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document\n * pixels. The next stroke fixes the offset from its first point. View state, no undo.\n */\n public setCloneSource(point: { x: number; y: number } | null): CloneSource | null {\n if (point && (!Number.isFinite(point.x) || !Number.isFinite(point.y))) throw new LayerOperationError('The clone source needs finite x and y (document pixels).');\n const source = point ? { x: Math.round(point.x), y: Math.round(point.y), offset: null } : null;\n this.state.paint = { ...this.state.paint, cloneSource: source };\n this.notify();\n return source;\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 === 'shape') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a shape layer; its pixels are drawn from its paths. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);\n if (layer.type === 'type') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a type layer; its pixels are drawn from its text. Rasterize it first (rasterize_layer), or ${action} on its mask (channel 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 (!STROKE_TOOLS.includes(tool)) throw new LayerOperationError(`Unknown paint tool \"${String(tool)}\"; use one of ${STROKE_TOOLS.join(', ')}.`);\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : isRetouchTool(tool) ? 'retouch' : '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 if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply);\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** A retouching stroke's start: its settings, what it samples and how it composites. */\n private retouchStroke(tool: RetouchTool, init: StrokeInit, brush: BrushSettings, target: ReturnType<ImageEditorController['paintTarget']>, apply: StrokeApply): RetouchStrokeInit {\n const settings = normalizeRetouch(init.retouch ?? {}, this.state.paint.retouch);\n const mask = target.channel !== 'pixels';\n if (tool === 'sponge' && mask) throw new LayerOperationError('The Sponge changes saturation, and a mask has none; target pixels (channel pixels), or use dodge or burn on the mask.');\n if (tool === 'smudge') return { kind: 'dabs', create: (pixels) => new RetouchDabStroke(brush, pixels, smudgeEffect(brush, settings.strength)) };\n if (tool === 'sponge') return { kind: 'dabs', create: (pixels) => new RetouchDabStroke(brush, pixels, spongeEffect(brush, settings.spongeMode === 'saturate', settings.vibrance)) };\n const retouchApply: RetouchApply = {\n opacity: tool === 'clone' || tool === 'heal' ? brush.opacity : 1,\n blend: tool === 'clone' && !mask ? brush.blend : 'normal',\n lockTransparency: target.lockTransparency,\n selection: apply.selection,\n offset: target.offset,\n };\n const coverage = new StrokeCoverage(brush);\n if (tool === 'dodge' || tool === 'burn') {\n const paint: RetouchPaint = { kind: 'tone', tone: toneOf(tool, settings) };\n return { kind: 'coverage', coverage, apply: retouchApply, start: () => {}, paint: () => paint };\n }\n // Masks sample only themselves; pixels sample the layer or the composite.\n const sample = mask ? 'current' : settings.sample;\n const own = this.surfaceSource(target.surface, target.offset);\n const source = sample === 'current' ? own : this.compositeSource(sample === 'all' ? null : target.id);\n const pad = (r: CropBox, m: number): CropBox => ({ x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m });\n const covered = (cov: CoverageMap, r: CropBox) => {\n const m = new Uint8Array(r.width * r.height);\n for (let y = 0; y < r.height; y++) for (let x = 0; x < r.width; x++) m[y * r.width + x] = cov.get(r.x + x, r.y + y) > 0 ? 1 : 0;\n return m;\n };\n if (tool === 'spot-heal') {\n const { width, height } = this.naturalSize;\n let patch: { x: number; y: number } | null = null;\n const trail: RetouchPaint = { kind: 'color', color: [0, 0, 0, 96] };\n return {\n kind: 'coverage',\n coverage,\n apply: retouchApply,\n start: () => {},\n paint: () => trail,\n finalPaint: (cov) => {\n if (!cov.bounds) return null;\n const win = spotHealWindow(cov.bounds, { width, height });\n if (win.width <= 0 || win.height <= 0) return null;\n const img = readSourceRegion(source, win);\n const hole = covered(cov, win);\n const healed = spotHealPixels(win.width, win.height, img, hole, brush.size, settings.spotType, (off) => {\n patch = off ? { x: off[0], y: off[1] } : null;\n });\n return { kind: 'source', source: new RegionPixelSource(win, healed), dx: 0, dy: 0 };\n },\n result: () => ({ spotHeal: { offset: patch } }),\n };\n }\n // Clone Stamp and Healing Brush: the offset from the source point.\n if (init.source) this.setCloneSource(init.source);\n const cs = this.state.paint.cloneSource;\n if (!cs) throw new LayerOperationError(`The ${tool === 'clone' ? 'Clone Stamp' : 'Healing Brush'} needs a source point; Alt-click the canvas, call set_clone_source, or pass source { x, y }.`);\n let offset = { x: 0, y: 0 };\n let first = { x: 0, y: 0 };\n const paint = (): RetouchPaint => ({ kind: 'source', source, dx: offset.x, dy: offset.y });\n return {\n kind: 'coverage',\n coverage,\n apply: retouchApply,\n start: (p) => {\n first = { x: Math.round(p.x), y: Math.round(p.y) };\n offset = settings.aligned && cs.offset ? { ...cs.offset } : { x: cs.x - first.x, y: cs.y - first.y };\n if (settings.aligned) this.state.paint = { ...this.state.paint, cloneSource: { x: cs.x, y: cs.y, offset: { ...offset } } };\n },\n paint,\n finalPaint:\n tool === 'heal'\n ? (cov) => {\n if (!cov.bounds) return null;\n const g = pad(cov.bounds, 2);\n const src = readSourceRegion(source, g, offset.x, offset.y);\n // The texture is fitted to what is seen: the layer, or the composite when sampling several layers.\n const dst = readSourceRegion(source, g);\n return { kind: 'source', source: new RegionPixelSource(g, healPixels(g.width, g.height, src, dst, covered(cov, g))), dx: 0, dy: 0 };\n }\n : undefined,\n result: () => ({ clone: { offset: { ...offset }, source: { x: first.x + offset.x, y: first.y + offset.y }, aligned: settings.aligned } }),\n };\n }\n\n /** A pixel source over a surface (layer pixels or a mask) in layer space at `offset`. */\n private surfaceSource(surface: TiledSurface, offset: { x: number; y: number }): PixelSource {\n return new TilePixelSource((tx, ty) => surface.tiles.get(tileKey(tx, ty))?.data ?? (surface.defaultValue ? defaultTile(surface) : null), surface.channels, offset);\n }\n\n /** A pixel source over the composite of the current layers (through `throughId` and below, or all of them), as it is now. */\n private compositeSource(throughId: string | null): PixelSource {\n const stack = withoutAnnotations(this.state.layers);\n const layers = throughId ? layersThrough(stack, throughId) : stack;\n const compositor = new LayerCompositor();\n const sources = this.compositorSources();\n return new TilePixelSource((tx, ty) => compositor.tile(layers, tx, ty, sources), 4);\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 /** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */\n private annotationLayer(id: string): AnnotationLayer | ShapeLayer | TypeLayer | undefined {\n return flattenLayers(this.state.layers).find((l): l is AnnotationLayer | ShapeLayer | TypeLayer => (l.type === 'annotation' || l.type === 'shape' || l.type === 'type') && annotationOfLayer(l)?.id === id);\n }\n\n /** What migrating an annotation needs: the canvas size, patterns and the text shaper. */\n private annotationEnv(): ShapeEnv & TextEnv {\n return { ...this.shapeEnv(), ...this.textEnv() };\n }\n\n /**\n * Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,\n * rectangle, ellipse, line and arrow annotations become shape layers that remember the\n * annotation (so `annotations` and `updateAnnotation` keep working); text and images stay\n * annotation layers.\n */\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, this.annotationEnv());\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 const current = layer ? annotationOfLayer(layer) : null;\n if (layer && current) {\n const annotation = { ...current, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(current) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => this.layerForAnnotation(layer, annotation, name));\n }\n this.notify();\n }\n\n /** `layer` (an annotation, or a shape or type layer migrated from one) showing `annotation` instead, keeping its other properties. */\n private layerForAnnotation(layer: AnnotationLayer | ShapeLayer | TypeLayer, annotation: Annotation, name: string): ImageLayer {\n const fresh = annotationLayerFor(annotation, this.annotationEnv());\n const keep: Partial<ImageLayer> = { id: layer.id, visible: layer.visible, opacity: layer.opacity };\n for (const k of ['clipped', 'mask', 'locks', 'effects'] as const) if (layer[k] !== undefined) (keep as Record<string, unknown>)[k] = layer[k];\n if (fresh.type !== 'annotation' && layer.type !== 'annotation' && layer.fillOpacity !== undefined) (keep as { fillOpacity?: number }).fillOpacity = layer.fillOpacity;\n // An annotation layer's own blend (set on the layer) wins over the annotation's.\n const blend = fresh.type === 'annotation' ? (layer.type === 'annotation' ? layer.blend : 'normal') : layer.type === 'annotation' && layer.blend !== 'normal' ? layer.blend : fresh.blend;\n return { ...fresh, ...keep, name, blend } as ImageLayer;\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 /** Deletes every annotation layer and every shape layer that still is the annotation it was migrated from. */\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation' || ((l.type === 'shape' || l.type === 'type') && l.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 // --- Type layers ---\n\n private textShaperValue: TextShaper | null | undefined;\n\n /** The shaper that lays out and draws type layers (`null` when there is none here). */\n public getTextShaper(): TextShaper | null {\n if (this.textShaperValue === undefined) this.textShaperValue = createCanvasTextShaper({ createCanvas: (w, h) => this.renderer.createCanvas(w, h) });\n return this.textShaperValue;\n }\n\n /** Sets the text shaper (fonts) type layers lay out and draw with; existing layers redraw on their next edit. */\n public setTextShaper(shaper: TextShaper | null): void {\n this.textShaperValue = shaper;\n this.notify();\n }\n\n /** What type layers lay out and draw with. */\n private textEnv(): TextEnv {\n return { ...this.naturalSize, shaper: this.getTextShaper(), createCanvas: (w, h) => this.renderer.createCanvas(w, h) };\n }\n\n /** Font families the shaper offers (generic families at least). */\n public listFonts(): string[] {\n return this.getTextShaper()?.families?.() ?? ['sans-serif', 'serif', 'monospace'];\n }\n\n private typeInfo(id: string): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const ranges = <S>(runs: ReadonlyArray<{ length: number; style: S }>) => {\n let at = 0;\n return runs.map((r) => {\n const out = { start: at, end: at + r.length, style: JSON.parse(JSON.stringify(r.style)) as S };\n at += r.length;\n return out;\n });\n };\n const shaper = this.getTextShaper();\n let lines: TypeLayerInfo['lines'] = [];\n let missing = layer.missingFonts ?? [];\n if (shaper) {\n const layout = layoutText(layer, shaper);\n const r = (n: number) => Math.round(n * 1000) / 1000;\n lines = layout.lines.map((l) => ({ start: l.start, end: l.end, baseline: r(l.baseline), width: r(l.width) }));\n if (layer.rendering === 'stored') missing = layout.missingFonts;\n }\n const info: TypeLayerInfo = {\n id: layer.id,\n name: layer.name,\n text: layer.text,\n runs: ranges(layer.runs),\n paragraphs: ranges(layer.paragraphs),\n frame: { ...layer.frame },\n transform: [...(layer.transform ?? [1, 0, 0, 1, 0, 0])] as unknown as AffineMatrix,\n lines,\n bounds: this.getLayerBounds(id),\n missingFonts: [...missing],\n rendering: layer.rendering === 'stored' ? 'stored' : 'live',\n };\n if (layer.annotation) info.annotationId = layer.annotation.id;\n return info;\n }\n\n /** A type layer's text, runs, paragraphs, frame, lines and bounds. */\n public getTextLayer(id: string): TypeLayerInfo {\n return this.typeInfo(id);\n }\n\n /**\n * Adds a type layer (one undo step) and makes it active: point text at `x`, `y`, or paragraph\n * text wrapped in `box`. Refused without a text shaper. Returns its id.\n */\n public addTextLayer(init: TextLayerInit): string {\n const text = normalizeText(init.text);\n if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError('x and y must be numbers (document pixels).');\n const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, (init.style ?? {}) as Record<string, unknown>);\n const paragraph = patchParagraphStyle({}, (init.paragraph ?? {}) as Record<string, unknown>);\n let frame: TypeLayer['frame'] = { kind: 'point' };\n if (init.box) {\n const { width, height } = init.box;\n if (!(Number.isFinite(width) && width >= 1 && Number.isFinite(height) && height >= 1)) throw new LayerOperationError('box.width and box.height must be at least 1 pixel.');\n frame = { kind: 'box', width, height };\n }\n if (init.rotation !== undefined && !Number.isFinite(init.rotation)) throw new LayerOperationError('rotation must be a number of degrees.');\n if (init.opacity !== undefined && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be between 0 and 1.');\n if (init.blend !== undefined && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode \"${String(init.blend)}\".`);\n const t: AffineMatrix = [1, 0, 0, 1, init.x, init.y];\n const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;\n const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());\n if (init.opacity !== undefined) layer.opacity = init.opacity;\n if (init.blend !== undefined) layer.blend = init.blend;\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 * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at\n * `start`. One undo step unless `commitToHistory` is false (typing). Returns the layer.\n */\n public setText(id: string, text: string, range: { start?: number; end?: number } = {}, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const insert = normalizeText(text);\n const [start, end] = textRange(layer.text, range.start, range.end);\n const { layers } = updateTypeLayer(this.state.layers, id, (l) => replaceTextRange(l, start, end, insert), this.textEnv(), 'edit its text');\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /** Sets character style fields over [start, end) (default all), one undo step. Returns the layer. */\n public setTextStyle(id: string, style: Partial<TextStyle> & Record<string, unknown>, range: { start?: number; end?: number } = {}, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const [start, end] = textRange(layer.text, range.start, range.end);\n if (!Object.keys(style).length) throw new LayerOperationError('Give at least one style field to set.');\n // Validate once before touching the runs.\n patchTextStyle(DEFAULT_TEXT_STYLE, style);\n const { layers } = updateTypeLayer(\n this.state.layers,\n id,\n (l) => {\n if (start === end) {\n // An empty range styles the whole of an empty text (the caret's style).\n if (l.text.length) throw new LayerOperationError('The range is empty; give start < end (or leave both out for the whole text).');\n return { runs: [{ length: 0, style: patchTextStyle(styleAt(l.runs, 0, DEFAULT_TEXT_STYLE), style) }] };\n }\n return { runs: restyleRange(l.runs, start, end, (s) => patchTextStyle(s, style)) as TypeLayer['runs'] };\n },\n this.textEnv(),\n 'restyle its text'\n );\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /** Sets paragraph style fields on every paragraph that [start, end) touches (default all), one undo step. */\n public setParagraphStyle(id: string, style: ParagraphStyle & Record<string, unknown>, range: { start?: number; end?: number } = {}, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const [start, end] = textRange(layer.text, range.start, range.end);\n if (!Object.keys(style).length) throw new LayerOperationError('Give at least one paragraph field to set.');\n patchParagraphStyle({}, style);\n const { layers } = updateTypeLayer(\n this.state.layers,\n id,\n (l) => {\n let at = 0;\n const paragraphs = l.paragraphs.map((p) => {\n const a = at;\n at += p.length;\n const touches = (a < end && at > start) || (start === end && start >= a && (start < at || at === l.text.length));\n return touches || !l.text.length ? { length: p.length, style: patchParagraphStyle(p.style, style) } : p;\n });\n return { paragraphs };\n },\n this.textEnv(),\n 'restyle its paragraphs'\n );\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /**\n * Point text ↔ paragraph text: `{ kind: 'box', width, height }` wraps the text in a box (its\n * top-left where the first line's top-left is now), `{ kind: 'point' }` sets it on one line per\n * paragraph. Also resizes a box. One undo step.\n */\n public setTextFrame(id: string, frame: { kind: 'point' } | { kind: 'box'; width: number; height: number }, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n if (frame.kind === 'box' && !(Number.isFinite(frame.width) && frame.width >= 1 && Number.isFinite(frame.height) && frame.height >= 1)) throw new LayerOperationError('width and height must be at least 1 pixel.');\n if (frame.kind !== 'box' && frame.kind !== 'point') throw new LayerOperationError('kind must be point or box.');\n const shaper = this.getTextShaper();\n const { layers } = updateTypeLayer(\n this.state.layers,\n id,\n (l) => {\n if (frame.kind === 'box' && l.frame.kind === 'box') return { frame: { kind: 'box', width: frame.width, height: frame.height } };\n if (!shaper) throw new LayerOperationError('Changing between point and paragraph text needs a text shaper to measure the text.');\n const lines = layoutText(l, shaper).lines;\n const first = lines[0];\n // Keep the first line where it is: shift the text-space origin.\n const dx = frame.kind === 'box' ? (first ? first.x : 0) : 0;\n const dy = frame.kind === 'box' ? (first ? first.baseline - first.ascent : 0) : first ? first.baseline : 0;\n const m = textMatrix(l);\n const shifted: AffineMatrix = [m[0], m[1], m[2], m[3], m[0] * dx + m[2] * dy + m[4], m[1] * dx + m[3] * dy + m[5]];\n const nextFrame: TypeLayer['frame'] = frame.kind === 'box' ? { kind: 'box', width: frame.width, height: frame.height } : { kind: 'point' };\n return { frame: nextFrame, transform: shifted };\n },\n this.textEnv(),\n 'change its frame'\n );\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /**\n * Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and\n * layer properties (needs a shaper that gives outlines). The fill is the first run's colour;\n * `warnings` says when other colours were dropped. One undo step.\n */\n public convertTextToShape(id: string): { layerId: string; warnings: string[] } {\n const layer = requireTypeLayer(this.state.layers, id);\n requireUnlocked(layer, 'all', 'convert it');\n const shaper = this.getTextShaper();\n if (!shaper) throw new LayerOperationError('Convert to Shape needs a text shaper that gives glyph outlines; none is set here.');\n const parts = textOutlines(layer, shaper);\n const subpaths = parts.flatMap((p) => bezierToSubpaths(p.path));\n if (!subpaths.length) throw new LayerOperationError('The text draws nothing (it is empty or only spaces), so there is no shape to make.');\n const warnings: string[] = [];\n const colors = new Set(layer.runs.filter((r) => r.length > 0).map((r) => `${r.style.color}/${r.style.opacity ?? 1}`));\n if (colors.size > 1) warnings.push('The text had several colours; the shape takes the first one.');\n const missing = layoutText(layer, shaper).missingFonts;\n if (missing.length) warnings.push(`Missing fonts were drawn with a fallback: ${missing.join(', ')}.`);\n const first = layer.runs.find((r) => r.length > 0)?.style ?? DEFAULT_TEXT_STYLE;\n const fill: ShapePaint = first.opacity !== undefined && first.opacity !== 1 ? { type: 'color', color: first.color, opacity: first.opacity } : { type: 'color', color: first.color };\n const shape = createShapeLayer({ components: [{ op: 'combine', subpaths }], fill, stroke: null, name: layer.name }, this.shapeEnv(), layer.id);\n const keep: Partial<ShapeLayer> = { visible: layer.visible, opacity: layer.opacity, blend: layer.blend };\n for (const k of ['clipped', 'mask', 'locks', 'effects', 'fillOpacity'] as const) if (layer[k] !== undefined) (keep as Record<string, unknown>)[k] = layer[k];\n this.commitLayers(mapLayer(this.state.layers, id, () => ({ ...shape, ...keep }) as ShapeLayer));\n return { layerId: id, warnings };\n }\n\n /** The layout of a type layer (lines, characters, bounds in text space), or `null` without a shaper. */\n public getTextLayout(id: string): TextLayout | null {\n const layer = requireTypeLayer(this.state.layers, id);\n const shaper = this.getTextShaper();\n return shaper ? layoutText(layer, shaper) : null;\n }\n\n /** The text index nearest a document point (for clicks on a type layer). */\n public textIndexAt(id: string, point: ImagePoint): number {\n const layer = requireTypeLayer(this.state.layers, id);\n const layout = this.getTextLayout(id);\n return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;\n }\n\n /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */\n public textCaret(id: string, index: number): { top: ImagePoint; bottom: ImagePoint; line: number } | null {\n const layer = requireTypeLayer(this.state.layers, id);\n const layout = this.getTextLayout(id);\n if (!layout) return null;\n const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));\n const m = textMatrix(layer);\n const at = (x: number, y: number) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });\n return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };\n }\n\n // --- Shape layers ---\n\n /** What shape rendering needs: the canvas size and the document's patterns. */\n private shapeEnv(): ShapeEnv {\n return { ...this.naturalSize, pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null };\n }\n\n /** One component from tool or UI input (validated). */\n private componentFrom(spec: ShapeGeometryInput, path: string): ShapeComponent {\n const op = requireOperation(spec.op, `${path}op`);\n if (spec.shape !== undefined && spec.subpaths !== undefined) throw new LayerOperationError(`Give ${path}shape or ${path}subpaths, not both.`);\n if (spec.shape !== undefined) return liveComponent(normalizeLiveShape(spec.shape, `${path}shape`), op);\n if (spec.subpaths !== undefined) return { op, subpaths: normalizeSubpaths(spec.subpaths, `${path}subpaths`) };\n throw new LayerOperationError(`${path}shape (a live shape: { kind: rect | ellipse | polygon | star | line, … }) or ${path}subpaths (anchors or SVG path data) is required.`);\n }\n\n private shapeInfo(id: string): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const r = (n: number) => Math.round(n * 1000) / 1000;\n const box = (b: CropBox | null) => (b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null);\n const info: ShapeLayerInfo = {\n id: layer.id,\n name: layer.name,\n components: layer.components.map((c, index) => ({\n index,\n op: c.op,\n live: c.live ? JSON.parse(JSON.stringify(c.live)) : null,\n subpaths: roundSubpaths(c.subpaths),\n bounds: box(shapeGeometryBounds({ components: [c] })),\n })),\n fillRule: layer.fillRule ?? 'nonzero',\n fill: layer.fill ? JSON.parse(JSON.stringify(layer.fill)) : null,\n stroke: layer.stroke ? JSON.parse(JSON.stringify(layer.stroke)) : null,\n geometryBounds: box(shapeGeometryBounds(layer)),\n bounds: this.getLayerBounds(id),\n };\n if (layer.annotation) info.annotationId = layer.annotation.id;\n return info;\n }\n\n /** A shape layer's components, fill, stroke and bounds. */\n public getShapeLayer(id: string): ShapeLayerInfo {\n return this.shapeInfo(id);\n }\n\n /**\n * Adds a shape layer (one undo step) and makes it active: a live shape or subpaths (or several\n * `components`), filled with `fill` (default the foreground colour, except for a stroke-only\n * open path) and stroked with `stroke`. Goes above the active layer unless placed. Returns its id.\n */\n public addShapeLayer(init: ShapeLayerInit): string {\n const specs: ShapeGeometryInput[] = init.components?.length ? init.components : [init];\n if (init.components?.length && (init.shape !== undefined || init.subpaths !== undefined)) throw new LayerOperationError('Give components, or shape / subpaths, not both.');\n const components = specs.map((c, i) => this.componentFrom(c, init.components?.length ? `components[${i}].` : ''));\n const stroke = init.stroke ? normalizeShapeStroke(init.stroke, 'stroke') : null;\n const openOnly = components.every((c) => !c.live && c.subpaths.every((sp) => !sp.closed));\n const fill = init.fill === undefined ? (stroke && openOnly ? null : normalizeShapePaint(this.state.paint.foreground, 'fill')) : init.fill === null ? null : normalizeShapePaint(init.fill, 'fill');\n if (!fill && !stroke) throw new LayerOperationError('A shape with neither fill nor stroke draws nothing; give fill or stroke.');\n if (init.fillRule !== undefined && init.fillRule !== 'nonzero' && init.fillRule !== 'evenodd') throw new LayerOperationError('fillRule must be nonzero or evenodd.');\n if (init.opacity !== undefined && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be between 0 and 1.');\n if (init.blend !== undefined && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode \"${String(init.blend)}\".`);\n const layer = createShapeLayer({ components, fill, stroke, fillRule: init.fillRule, name: init.name, opacity: init.opacity, blend: init.blend }, this.shapeEnv());\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 * Sets a shape's fill, stroke or fill rule (one undo step unless `commitToHistory` is false):\n * `fill` / `stroke` replace them (`null` removes); `stroke` given as a partial object changes\n * only those settings of the current stroke. Returns the shape.\n */\n public setShapeStyle(\n id: string,\n changes: { fill?: ShapePaint | string | null; stroke?: ShapeStrokeInput | null; fillRule?: ShapeFillRule },\n commitToHistory = true\n ): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const fill = changes.fill === undefined ? layer.fill : changes.fill === null ? null : normalizeShapePaint(changes.fill, 'fill');\n const stroke = changes.stroke === undefined ? layer.stroke : changes.stroke === null ? null : normalizeShapeStroke(changes.stroke, 'stroke', layer.stroke);\n if (changes.fillRule !== undefined && changes.fillRule !== 'nonzero' && changes.fillRule !== 'evenodd') throw new LayerOperationError('fillRule must be nonzero or evenodd.');\n const fillRule = changes.fillRule ?? layer.fillRule ?? 'nonzero';\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ fill, stroke, fillRule: fillRule === 'nonzero' ? undefined : fillRule }), this.shapeEnv(), 'restyle it');\n this.commitLayers(layers, undefined, commitToHistory);\n return this.shapeInfo(id);\n }\n\n private componentIndex(layer: ShapeLayer, index: unknown): number {\n if (!Number.isInteger(index) || (index as number) < 0 || (index as number) >= layer.components.length) {\n throw new LayerOperationError(`componentIndex must be 0..${layer.components.length - 1} (get_shape_layer lists the components), got ${String(index)}.`);\n }\n return index as number;\n }\n\n /**\n * Changes one component (one undo step unless `commitToHistory` is false): its live shape\n * (fields not given keep their values when the kind stays the same), its subpaths (which drops\n * the live shape) or its operation. Returns the shape.\n */\n public setShapeComponent(id: string, index: number, change: ShapeGeometryInput, commitToHistory = true): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const i = this.componentIndex(layer, index);\n const current = layer.components[i];\n let shape = change.shape;\n if (shape && current.live && (shape as { kind?: unknown }).kind === undefined) shape = { ...current.live, ...shape } as LiveShape;\n else if (shape && current.live && (shape as { kind?: unknown }).kind === current.live.kind) shape = { ...current.live, ...shape } as LiveShape;\n const next = shape !== undefined || change.subpaths !== undefined ? this.componentFrom({ ...change, shape, op: change.op ?? current.op }, '') : { ...current, op: requireOperation(change.op) };\n const components = layer.components.map((c, k) => (k === i ? next : c));\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers, undefined, commitToHistory);\n return this.shapeInfo(id);\n }\n\n /** Adds a component to a shape (path operations), on top unless `index` is given (one undo step). Returns the shape. */\n public addShapeComponent(id: string, spec: ShapeGeometryInput, index?: number): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const c = this.componentFrom(spec, '');\n const at = index === undefined ? layer.components.length : index;\n if (!Number.isInteger(at) || at < 0 || at > layer.components.length) throw new LayerOperationError(`index must be 0..${layer.components.length}, got ${String(index)}.`);\n const components = [...layer.components.slice(0, at), c, ...layer.components.slice(at)];\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers);\n return this.shapeInfo(id);\n }\n\n /** Removes one component of a shape (one undo step); the last one cannot go (delete the layer instead). Returns the shape. */\n public removeShapeComponent(id: string, index: number): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const i = this.componentIndex(layer, index);\n if (layer.components.length === 1) throw new LayerOperationError('This is the shape\\'s only component; delete the layer instead (delete_layer).');\n const components = layer.components.filter((_, k) => k !== i);\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers);\n return this.shapeInfo(id);\n }\n\n /**\n * Merge Shape Components: the area the components combine to becomes one plain component (live\n * shapes become paths), as one undo step. Returns the shape.\n */\n public mergeShapeComponents(id: string): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n if (layer.components.length < 2) throw new LayerOperationError('The shape has one component; there is nothing to merge.');\n const components = mergeShapeComponentsOp(layer.components, layer.fillRule);\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components, fillRule: undefined }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers);\n return this.shapeInfo(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 if (layer.type === 'shape' || layer.type === 'type') {\n next = layer.type === 'shape' ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);\n if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;\n } else {\n throw new LayerOperationError(`Only shape, type and 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 = this.checkPsdFonts(result);\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 * Type layers whose fonts the shaper lacks: they keep Photoshop's pixels, are marked with their\n * `missingFonts`, and a warning names them (an edit draws them with a fallback).\n */\n private checkPsdFonts(result: PsdImportResult): ImageLayer[] {\n const shaper = this.getTextShaper();\n const types = flattenLayers(result.layers).filter((l): l is TypeLayer => l.type === 'type');\n if (!types.length) return result.layers;\n let layers = result.layers;\n const missing = new Map<string, string[]>();\n for (const l of types) {\n const fonts = shaper ? layoutText(l, shaper).missingFonts : [...new Set(l.runs.map((r) => r.style.fontFamily))];\n if (!fonts.length) continue;\n layers = mapLayer(layers, l.id, () => ({ ...l, missingFonts: fonts }));\n for (const f of fonts) missing.set(f, [...(missing.get(f) ?? []), l.name]);\n }\n for (const [font, names] of missing) {\n result.warnings.push(\n shaper\n ? `The font \"${font}\" is missing here: the type shows Photoshop's pixels until it is edited, then a fallback font (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`\n : `Type layers show Photoshop's pixels; nothing here can draw text, so they cannot be edited (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`\n );\n if (!shaper) break;\n }\n return layers;\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 === 'shape' || layer.type === 'type') {\n // Written as Photoshop shape and type layers (with their pixels for viewers that need them).\n if (layer.type === 'type' && layer.missingFonts?.length) add(`Type layers drawn with a fallback for missing fonts (${layer.missingFonts.join(', ')}) keep the font names, so Photoshop may draw them differently`, layer.name);\n const out = { ...layer };\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n }\n if (layer.type === 'smart') {\n const out = { ...layer };\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\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 const env: ShapeEnv = { ...next.dimensions, pattern: (pid) => this.state.patterns?.find((p) => p.id === pid) ?? null };\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 } else if (l.type === 'shape') {\n const a = l.annotation;\n const scaled = a ? scaleAnnotation(a, sx, sy) : null;\n const fromAnnotation = scaled ? shapeFromAnnotation(scaled) : null;\n const m: AffineMatrix = [sx, 0, 0, sy, 0, 0];\n const components = fromAnnotation?.components ?? transformShapeComponents(l.components, (p) => ({ x: p.x * sx, y: p.y * sy }), m);\n const moved: ShapeLayer = { ...l, components, ...(fromAnnotation ? { fill: fromAnnotation.fill, stroke: fromAnnotation.stroke } : {}) };\n if (scaled && fromAnnotation) moved.annotation = scaled;\n else delete moved.annotation;\n layers = mapLayer(layers, l.id, () => rerenderShape(moved, env));\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' || ((l.type === 'shape' || l.type === 'type') && l.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 const storedPatterns = deserializePatterns(data.patterns, []);\n s.layers = deserializeLayers(data.layers, {\n assets: data.assets ?? {},\n problems,\n smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],\n canvas,\n pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null,\n text: this.textEnv(),\n });\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 === 'shape' || l.type === 'type') collectTiles(l.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 with every layer above `id` left out (Sample: Current & Below). */\nfunction layersThrough(layers: readonly ImageLayer[], id: string): ImageLayer[] {\n const walk = (list: readonly ImageLayer[]): { out: ImageLayer[]; found: boolean } => {\n const out: ImageLayer[] = [];\n for (const l of list) {\n if (l.id === id) {\n out.push(l);\n return { out, found: true };\n }\n if (l.type === 'group') {\n const sub = walk(l.children);\n if (sub.found) {\n out.push({ ...l, children: sub.out });\n return { out, found: true };\n }\n }\n out.push(l);\n }\n return { out, found: false };\n };\n return walk(layers).out;\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' || ((l.type === 'shape' || l.type === 'type') && l.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', 'shape', 'type']);\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\nexport interface ImageFilterPreset {\n id: string;\n label: string;\n adjustments: Partial<ImageAdjustments>;\n}\n\n/** Built-in colour-grade presets for quick one-click adjustment looks. */\nexport const IMAGE_FILTER_PRESETS: readonly ImageFilterPreset[] = [\n {\n id: 'none',\n label: 'Original',\n adjustments: {\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 },\n },\n {\n id: 'dramatic',\n label: 'Dramatic',\n adjustments: {\n brightness: -5,\n contrast: 45,\n saturation: 20,\n exposure: 5,\n temperature: -10,\n tint: 0,\n },\n },\n {\n id: 'cyberpunk',\n label: 'Cyberpunk',\n adjustments: {\n brightness: 5,\n contrast: 35,\n saturation: 55,\n exposure: 0,\n temperature: -25,\n tint: 40,\n },\n },\n {\n id: 'vintage',\n label: 'Vintage',\n adjustments: {\n brightness: 8,\n contrast: -15,\n saturation: -35,\n exposure: 5,\n temperature: 35,\n tint: 10,\n },\n },\n {\n id: 'warm',\n label: 'Warm',\n adjustments: {\n brightness: 5,\n contrast: 10,\n saturation: 15,\n exposure: 0,\n temperature: 45,\n tint: 5,\n },\n },\n {\n id: 'cool',\n label: 'Cool',\n adjustments: {\n brightness: 0,\n contrast: 15,\n saturation: 10,\n exposure: 0,\n temperature: -45,\n tint: -10,\n },\n },\n {\n id: 'grayscale',\n label: 'B&W',\n adjustments: {\n brightness: 0,\n contrast: 20,\n saturation: -100,\n exposure: 0,\n temperature: 0,\n tint: 0,\n },\n },\n {\n id: 'noir',\n label: 'Noir',\n adjustments: {\n brightness: -15,\n contrast: 65,\n saturation: -100,\n exposure: -5,\n temperature: 0,\n tint: 0,\n },\n },\n {\n id: 'fade',\n label: 'Fade',\n adjustments: {\n brightness: 15,\n contrast: -25,\n saturation: -20,\n exposure: 10,\n temperature: 10,\n tint: 0,\n },\n },\n];\n\nexport interface ImageCropPreset {\n id: string;\n label: string;\n aspectRatio: number | null;\n}\n\n/** Standard aspect-ratio presets for cropping. */\nexport const IMAGE_CROP_PRESETS: readonly ImageCropPreset[] = [\n { id: 'free', label: 'Free', aspectRatio: null },\n { id: '1:1', label: '1:1', aspectRatio: 1 },\n { id: '16:9', label: '16:9', aspectRatio: 16 / 9 },\n { id: '4:3', label: '4:3', aspectRatio: 4 / 3 },\n { id: '4:5', label: '4:5', aspectRatio: 4 / 5 },\n { id: '9:16', label: '9:16', aspectRatio: 9 / 16 },\n];\n\n/** Numeric controls for depth-mask near/far threshold range. */\nexport const DEPTH_MASK_CONTROLS: readonly ControlSpec[] = [\n {\n id: 'near',\n label: 'Near Threshold',\n section: 'effects',\n min: 0,\n max: 255,\n step: 1,\n default: 0,\n decimals: 0,\n track: plain,\n },\n {\n id: 'far',\n label: 'Far Threshold',\n section: 'effects',\n min: 0,\n max: 255,\n step: 1,\n default: 255,\n decimals: 0,\n track: plain,\n },\n];\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 'shape':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.shape = {\n components: layer.components.map((c) => ({ op: c.op, kind: c.live?.kind ?? 'path' })),\n fill: layer.fill ? layer.fill.type : null,\n stroke: layer.stroke ? { width: layer.stroke.width, paint: layer.stroke.paint.type } : null,\n };\n if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'type':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.text = {\n text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}…` : layer.text,\n frame: layer.frame.kind,\n runs: layer.runs.length,\n rendering: layer.rendering === 'stored' ? 'stored' : 'live',\n missingFonts: layer.missingFonts ?? [],\n };\n if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\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, shape, type, smart, annotation), name, document bounds (pixel, shape, type and smart 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), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), a text summary (the text, point or box frame, run count, live or stored pixels, missing fonts; details from get_text_layer), 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 a shape or type layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, 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, shape, smart object, 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, shape, type, smart object, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.\",\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, S, J, Shift+J, O, Shift+O, [ ], Shift+[ ],\n * number keys, D, X) and a pointer gesture that turns pointer events into strokes (Brush, Mixer\n * Brush, Eraser and the retouching tools; Alt-click sets the clone source) or layer moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { shapeGeometryBounds } from './shape';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type MixerSettings, type RetouchSettings } 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 { id: 'clone', label: 'Clone', shortcut: 'S' },\n { id: 'heal', label: 'Heal', shortcut: 'Shift+J' },\n { id: 'spot-heal', label: 'Spot Heal', shortcut: 'J' },\n { id: 'smudge', label: 'Smudge', shortcut: '' },\n { id: 'dodge', label: 'Dodge', shortcut: 'O' },\n { id: 'burn', label: 'Burn', shortcut: 'Shift+O' },\n { id: 'sponge', label: 'Sponge', shortcut: 'Shift+O' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => PAINT_TOOLS.some((t) => t.id === tool);\n\n/** The paint tools that retouch (their options show instead of the brush colours). */\nexport const isRetouchPaintTool = (tool: string): boolean =>\n tool === 'clone' || tool === 'heal' || tool === 'spot-heal' || tool === 'smudge' || tool === 'dodge' || tool === 'burn' || tool === 'sponge';\n\n/** Retouch sliders: dodge / burn exposure and smudge strength (shown in %). */\nexport const RETOUCH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'exposure' | 'strength'; tools: readonly PaintTool[] })[] = [\n { id: 'exposure', label: 'Exposure', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%', tools: ['dodge', 'burn'] },\n { id: 'strength', label: 'Strength', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%', tools: ['smudge'] },\n];\n\n/** Retouch choices, each a row of toggle buttons, with the tools that show it. */\nexport const RETOUCH_CHOICES: readonly {\n id: 'sample' | 'spotType' | 'range' | 'spongeMode';\n label: string;\n tools: readonly PaintTool[];\n options: readonly { id: string; label: string; title: string }[];\n}[] = [\n {\n id: 'sample',\n label: 'Sample',\n tools: ['clone', 'heal', 'spot-heal'],\n options: [\n { id: 'current', label: 'Current', title: 'Sample the layer being retouched' },\n { id: 'current-below', label: 'Current & Below', title: 'Sample this layer and the layers under it' },\n { id: 'all', label: 'All', title: 'Sample every layer (retouch onto an empty layer)' },\n ],\n },\n {\n id: 'spotType',\n label: 'Type',\n tools: ['spot-heal'],\n options: [\n { id: 'proximity-match', label: 'Proximity Match', title: 'Copy the best matching patch nearby' },\n { id: 'smooth', label: 'Smooth', title: 'Fill smoothly from the edges' },\n ],\n },\n {\n id: 'range',\n label: 'Range',\n tools: ['dodge', 'burn'],\n options: [\n { id: 'shadows', label: 'Shadows', title: 'Change the dark tones most' },\n { id: 'midtones', label: 'Midtones', title: 'Change the middle tones most' },\n { id: 'highlights', label: 'Highlights', title: 'Change the light tones most' },\n ],\n },\n {\n id: 'spongeMode',\n label: 'Mode',\n tools: ['sponge'],\n options: [\n { id: 'desaturate', label: 'Desaturate', title: 'Take colour out' },\n { id: 'saturate', label: 'Saturate', title: 'Add colour' },\n ],\n },\n];\n\n/** Retouch on / off options, with the tools that show each. */\nexport const RETOUCH_TOGGLES: readonly { id: 'aligned' | 'protectTones' | 'vibrance'; label: string; tools: readonly PaintTool[] }[] = [\n { id: 'aligned', label: 'Aligned', tools: ['clone', 'heal'] },\n { id: 'protectTones', label: 'Protect tones', tools: ['dodge', 'burn'] },\n { id: 'vibrance', label: 'Vibrance', tools: ['sponge'] },\n];\n\n/** Sets one retouch choice from the options bar. */\nexport function setRetouchChoice(controller: ImageEditorController, id: (typeof RETOUCH_CHOICES)[number]['id'], value: string): void {\n controller.setPaintOptions({ retouch: { [id]: value } as Partial<RetouchSettings> });\n}\n\n/** What the options bar says about the clone source. */\nexport function cloneSourceLabel(controller: ImageEditorController): string {\n const cs = controller.getState().paint.cloneSource;\n return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ''}` : 'Alt-click to set the source';\n}\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 's':\n controller.setTool('clone');\n return true;\n case 'j':\n controller.setTool(e.shiftKey || key === 'J' ? (controller.getState().activeTool === 'heal' ? 'spot-heal' : 'heal') : 'spot-heal');\n return true;\n case 'o': {\n // O: Dodge; Shift+O cycles Dodge → Burn → Sponge.\n const cur = controller.getState().activeTool;\n const next = e.shiftKey || key === 'O' ? (cur === 'dodge' ? 'burn' : cur === 'burn' ? 'sponge' : 'dodge') : 'dodge';\n controller.setTool(next);\n return true;\n }\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 /** Alt (Option) held on the press: with the Clone Stamp or Healing Brush it sets the source. */\n altKey?: boolean;\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' && layer.type !== 'shape' && layer.type !== 'smart') throw new Error(`Only pixel, shape and smart object 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 // Where `offsetLayer` measures the position: the offset, a shape's top-left, a smart object's first corner.\n const origin = layer.type === 'pixel' ? { ...layer.offset } : layer.type === 'smart' ? { ...layer.placement.corners[0] } : (() => {\n const b = shapeGeometryBounds(layer);\n return b ? { x: b.x, y: b.y } : { x: 0, y: 0 };\n })();\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };\n return null;\n }\n if ((tool === 'clone' || tool === 'heal') && sample.altKey) {\n this.controller.setCloneSource({ x: sample.x, y: sample.y });\n return null;\n }\n if (isPaintTool(tool)) {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([{ x: sample.x, y: sample.y, pressure: sample.pressure }]);\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, RetouchTool } from './controller';\nimport { MAX_BRUSH_SIZE, RETOUCH_SAMPLES, SPOT_HEAL_TYPES, TONE_RANGES, type BrushSettings, type RetouchSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\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;\n\nconst POINTS = {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The stroke path in document pixels; one point is 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} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush tip; 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). Retouching usually wants a soft tip (0..0.5).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity (Clone Stamp and Healing Brush only; default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab (default 1). For the Sponge, how strongly each pass moves saturation.' },\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 },\n additionalProperties: false,\n} as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to retouch (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 filter mask. Masks are grey values and sample only themselves.\",\n },\n} as const;\n\nconst SAMPLE = {\n type: 'string',\n enum: [...RETOUCH_SAMPLES],\n description:\n 'What is sampled: current (default: the target layer), current-below (the composite of this layer and everything under it), or all (the composite of every layer). Sample current-below or all to retouch onto an empty layer above the image.',\n} as const;\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\nfunction checkSource(controller: ImageEditorController, source: unknown): { x: number; y: number } | undefined {\n if (source === undefined) return undefined;\n const p = source as { x: number; y: number };\n const { width, height } = controller.getState().imageDimensions;\n if (!(p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {\n throw new LayerOperationError(`source (${p.x}, ${p.y}) is outside the ${width} × ${height} canvas; pick a point on the image to sample.`);\n }\n return p;\n}\n\nconst SOURCE = {\n type: 'object',\n description: 'Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).',\n properties: { x: { type: 'number', description: 'Source x in px.' }, y: { type: 'number', description: 'Source y in px.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\n\n/**\n * Agent tools for the retouching tools, one per tool: `clone_stamp`, `heal_brush`, `spot_heal`,\n * `smudge`, `dodge`, `burn` and `sponge` (each one stroke, one undo step, returning the layer,\n * channel, changed bounds and dab count), plus the view state they share: `get_retouch_options`,\n * `set_retouch_options` and `set_clone_source`.\n */\nexport function createImageRetouchTools(controller: ImageEditorController): AgentTool[] {\n const stroke = (tool: RetouchTool, args: Record<string, unknown>, retouch: Partial<RetouchSettings>) =>\n controller.paintStroke({\n tool,\n points: args.points as StrokePoint[],\n brush: args.brush as Partial<BrushSettings> | undefined,\n retouch,\n source: checkSource(controller, args.source),\n ...readTarget(args),\n });\n\n const cloneStamp: AgentTool = {\n name: 'clone_stamp',\n title: 'Clone Stamp stroke',\n description:\n \"Copies pixels from the clone source along a stroke (Photoshop's Clone Stamp): each point paints what lies at point + offset, where the offset is source − the stroke's first point. With aligned on, later strokes keep that offset; off, every stroke starts again at the source point. Samples the layer, the layers up to it, or all layers, from before the stroke. The selection limits it; a transparency-locked layer keeps its alpha. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n source: SOURCE,\n aligned: { type: 'boolean', description: 'Keep the offset for the next strokes (default the current option, true).' },\n sample: SAMPLE,\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Clone Stamp stroke', ...WRITE },\n handler: (args) => stroke('clone', args, pick(args, ['aligned', 'sample'])),\n };\n\n const healBrush: AgentTool = {\n name: 'heal_brush',\n title: 'Healing Brush stroke',\n description:\n \"Heals along a stroke from the clone source (Photoshop's Healing Brush): the source's texture is cloned, then blended so its colour and light match the pixels around the stroke (Poisson seamless cloning, applied when the stroke ends). Use it for blemishes and dust where the area around them is the right tone; use clone_stamp to copy exactly. Same source, offset and aligned rules as clone_stamp. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n source: SOURCE,\n aligned: { type: 'boolean', description: 'Keep the offset for the next strokes (default the current option, true).' },\n sample: SAMPLE,\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Healing Brush stroke', ...WRITE },\n handler: (args) => stroke('heal', args, pick(args, ['aligned', 'sample'])),\n };\n\n const spotHeal: AgentTool = {\n name: 'spot_heal',\n title: 'Spot Healing stroke',\n description:\n \"Removes what a stroke covers without a source point (Photoshop's Spot Healing Brush): proximity-match copies the best matching patch nearby and blends it in; smooth fills from the edges inwards (for spots on smooth areas). Paint just over the spot with a brush a little larger than it. One undo step. Returns the layerId, channel, changed bounds, dab count and spotHeal {offset} (the patch offset copied, or null for a smooth fill).\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n type: { type: 'string', enum: [...SPOT_HEAL_TYPES], description: 'proximity-match (default) or smooth.' },\n sample: SAMPLE,\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Spot Healing stroke', ...WRITE },\n handler: (args) => stroke('spot-heal', args, { ...pick(args, ['sample']), ...(args.type !== undefined ? { spotType: args.type as RetouchSettings['spotType'] } : {}) }),\n };\n\n const smudge: AgentTool = {\n name: 'smudge',\n title: 'Smudge stroke',\n description:\n \"Drags pixels along a stroke like a finger through wet paint (Photoshop's Smudge): each dab lays what the finger carries and picks up what was under it, so colour is pulled in the stroke's direction and fades with distance. One undo step. Returns the layerId, channel, changed bounds and dab count.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'How far colour is dragged, 0..1 (default the current option, 0.5).' },\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Smudge stroke', ...WRITE },\n handler: (args) => stroke('smudge', args, pick(args, ['strength'])),\n };\n\n const toneSchema = (verb: string) => ({\n type: 'object' as const,\n properties: {\n points: POINTS,\n range: { type: 'string', enum: [...TONE_RANGES], description: `Which tones to ${verb} most: shadows, midtones (default the current option, midtones) or highlights.` },\n exposure: { type: 'number', minimum: 0, maximum: 1, description: 'Strength 0..1 (default the current option, 0.5). Overlapping dabs in one stroke never go past it; another stroke builds on it.' },\n protectTones: { type: 'boolean', description: 'Change luma only, keeping hue and saturation (default true).' },\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n }) as const;\n\n const dodge: AgentTool = {\n name: 'dodge',\n title: 'Dodge stroke',\n description:\n \"Lightens the pixels a stroke covers (Photoshop's Dodge), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count. Inspect the result with capture_view or get_histogram; several light strokes look more natural than one strong one.\",\n inputSchema: toneSchema('lighten'),\n annotations: { title: 'Dodge stroke', ...WRITE },\n handler: (args) => stroke('dodge', args, pick(args, ['range', 'exposure', 'protectTones'])),\n };\n\n const burn: AgentTool = {\n name: 'burn',\n title: 'Burn stroke',\n description:\n \"Darkens the pixels a stroke covers (Photoshop's Burn), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count.\",\n inputSchema: toneSchema('darken'),\n annotations: { title: 'Burn stroke', ...WRITE },\n handler: (args) => stroke('burn', args, pick(args, ['range', 'exposure', 'protectTones'])),\n };\n\n const spongeTool: AgentTool = {\n name: 'sponge',\n title: 'Sponge stroke',\n description:\n \"Saturates or desaturates the pixels a stroke covers (Photoshop's Sponge); passes compound, and brush.flow sets how much each moves. Pixels only (a mask has no colour). One undo step. Returns the layerId, channel, changed bounds and dab count.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n mode: { type: 'string', enum: ['desaturate', 'saturate'], description: 'desaturate (default the current option, desaturate) or saturate.' },\n vibrance: { type: 'boolean', description: 'Damp the change where colours would clip or collapse to grey (default true).' },\n brush: BRUSH,\n layerId: TARGET.layerId,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Sponge stroke', ...WRITE },\n handler: (args) =>\n stroke('sponge', { ...args, channel: 'pixels' }, { ...pick(args, ['vibrance']), ...(args.mode !== undefined ? { spongeMode: args.mode as RetouchSettings['spongeMode'] } : {}) }),\n };\n\n const getOptions: AgentTool = {\n name: 'get_retouch_options',\n title: 'Get retouch options',\n description:\n 'Reads the retouching tools\\' options (aligned, sample, spotType, range, exposure, protectTones, spongeMode, vibrance, strength) and the clone source ({x, y, offset} or null; offset is the aligned offset the last clone or heal stroke fixed). Call it before clone_stamp or heal_brush without a source to see where they will sample.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get retouch options', ...READ },\n handler: () => {\n const { retouch, cloneSource } = controller.getState().paint;\n return { options: { ...retouch }, cloneSource: cloneSource ? { ...cloneSource } : null };\n },\n };\n\n const setOptions: AgentTool = {\n name: 'set_retouch_options',\n title: 'Set retouch options',\n description:\n 'Sets the retouching tools\\' defaults (view state, not an undo step); each stroke tool can also override them per call. Changing aligned clears the aligned offset. Returns the options after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n aligned: { type: 'boolean', description: 'Clone / heal: keep the offset from stroke to stroke (default true).' },\n sample: SAMPLE,\n spotType: { type: 'string', enum: [...SPOT_HEAL_TYPES], description: 'Spot heal: proximity-match (default) or smooth.' },\n range: { type: 'string', enum: [...TONE_RANGES], description: 'Dodge / burn: shadows, midtones (default) or highlights.' },\n exposure: { type: 'number', minimum: 0, maximum: 1, description: 'Dodge / burn strength 0..1 (default 0.5).' },\n protectTones: { type: 'boolean', description: 'Dodge / burn: keep hue and saturation (default true).' },\n spongeMode: { type: 'string', enum: ['desaturate', 'saturate'], description: 'Sponge: desaturate (default) or saturate.' },\n vibrance: { type: 'boolean', description: 'Sponge: damp clipping (default true).' },\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'Smudge strength 0..1 (default 0.5).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set retouch options', ...WRITE, idempotentHint: true },\n handler: (args) => ({ ...controller.setPaintOptions({ retouch: args as Partial<RetouchSettings> }).retouch }),\n };\n\n const setSource: AgentTool = {\n name: 'set_clone_source',\n title: 'Set the clone source',\n description:\n \"Sets the Clone Stamp and Healing Brush source point (Photoshop's Alt-click), in document pixels, or clears it with clear true. The next clone_stamp or heal_brush stroke fixes the offset from its first point. View state, not an undo step. Returns the clone source {x, y, offset} (or null).\",\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Source x in px (required unless clear).' },\n y: { type: 'number', description: 'Source y in px (required unless clear).' },\n clear: { type: 'boolean', description: 'Remove the source point instead (default false).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the clone source', ...WRITE, idempotentHint: true },\n handler: (args) => {\n if (args.clear === true) {\n if (args.x !== undefined || args.y !== undefined) throw new LayerOperationError('Give either clear true or x and y, not both.');\n return controller.setCloneSource(null);\n }\n if (typeof args.x !== 'number' || typeof args.y !== 'number') throw new LayerOperationError('Pass x and y (document pixels) for the source point, or clear true to remove it.');\n return controller.setCloneSource(checkSource(controller, { x: args.x, y: args.y })!);\n },\n };\n\n return [cloneStamp, healBrush, spotHeal, smudge, dodge, burn, spongeTool, getOptions, setOptions, setSource];\n}\n\nfunction pick(args: Record<string, unknown>, keys: string[]): Partial<RetouchSettings> {\n const out: Record<string, unknown> = {};\n for (const k of keys) if (args[k] !== undefined) out[k] = args[k];\n return out as Partial<RetouchSettings>;\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; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); shape layers come in as live shapes (paths, path operations, fill, stroke, live rectangles, ellipses and lines); type layers come in as live text (runs, paragraphs, frame, transform) showing Photoshop’s pixels until edited, with a warning for fonts missing here; fill layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).',\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). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Shape layers are written as Photoshop shape layers (path, fill, stroke, live shapes) and type layers as Photoshop type layers (text, runs, paragraphs, frame; blocks opened from a PSD are written back unchanged while the layer is). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor\\'s composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.',\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 } from '../tools/registry.js';\nimport type { ImageEditorController, ShapeGeometryInput, ShapeLayerInit } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError } from './layers/ops';\nimport { requireShapeLayer } from './layers/shape';\nimport { describeLayer } 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 LAYER_ID = { type: 'string', description: 'Shape layer id from list_layers (type shape) or add_shape_layer.' } as const;\nconst COMPONENT_INDEX = { type: 'integer', minimum: 0, description: 'Component index from get_shape_layer (0 = bottom component).' } as const;\nconst OP = {\n type: 'string',\n enum: ['combine', 'subtract', 'intersect', 'exclude'],\n description: 'How the component combines with the components below it (Photoshop path operations; default combine). The bottom component always combines.',\n} as const;\n\nconst NUM = (description: string) => ({ type: 'number', description }) as const;\nconst MATRIX = {\n type: 'array',\n items: { type: 'number', description: 'Matrix entry.' },\n minItems: 6,\n maxItems: 6,\n description: \"Optional affine map from the shape's own frame to the document [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f), e.g. a rotation. Default identity.\",\n} as const;\nconst BOX = {\n x: NUM('Left of the bounding box, document pixels.'),\n y: NUM('Top of the bounding box, document pixels.'),\n width: NUM('Width in pixels.'),\n height: NUM('Height in pixels.'),\n transform: { $ref: '#/$defs/matrix' },\n} as const;\n\n/** Shared parts, referenced as `#/$defs/...`. */\nconst DEFS = {\n matrix: MATRIX,\n point: {\n type: 'object',\n description: 'A point in document pixels.',\n properties: { x: NUM('Pixels from the left.'), y: NUM('Pixels from the top.') },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n arrowhead: {\n type: 'object',\n description: 'An arrowhead whose tip is the line end: a triangle, notched by concavity.',\n properties: {\n width: NUM('Width across the base, pixels.'),\n length: NUM('Length from base to tip, pixels.'),\n concavity: { type: 'number', minimum: -0.5, maximum: 0.5, description: 'Notch of the base towards the tip as a fraction of length (default 0).' },\n },\n required: ['width', 'length'],\n additionalProperties: false,\n },\n shape: {\n description:\n 'A live shape (stays editable as that shape): rect (with optional corner radii), ellipse, polygon, star, or line (a filled bar with optional arrowheads). Sizes are document pixels.',\n anyOf: [\n {\n type: 'object',\n description: 'Rectangle.',\n properties: {\n kind: { const: 'rect', description: 'Rectangle.' },\n ...BOX,\n radius: { type: 'number', minimum: 0, description: 'One corner radius for all corners, pixels.' },\n radii: { type: 'array', items: { type: 'number', minimum: 0, description: 'Radius in pixels.' }, minItems: 4, maxItems: 4, description: 'Corner radii [top-left, top-right, bottom-right, bottom-left] in pixels.' },\n },\n required: ['kind', 'x', 'y', 'width', 'height'],\n additionalProperties: false,\n },\n { type: 'object', description: 'Ellipse inscribed in the box.', properties: { kind: { const: 'ellipse', description: 'Ellipse.' }, ...BOX }, required: ['kind', 'x', 'y', 'width', 'height'], additionalProperties: false },\n {\n type: 'object',\n description: 'Regular polygon inscribed in the box, first vertex at the top.',\n properties: { kind: { const: 'polygon', description: 'Polygon.' }, ...BOX, sides: { type: 'integer', minimum: 3, maximum: 100, description: 'Number of sides (default 6).' } },\n required: ['kind', 'x', 'y', 'width', 'height'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Star inscribed in the box, first point at the top.',\n properties: {\n kind: { const: 'star', description: 'Star.' },\n ...BOX,\n points: { type: 'integer', minimum: 3, maximum: 100, description: 'Number of points (default 5).' },\n innerRatio: { type: 'number', minimum: 0.01, maximum: 1, description: 'Inner radius over outer radius (default 0.5).' },\n },\n required: ['kind', 'x', 'y', 'width', 'height'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Line drawn as a filled bar.',\n properties: {\n kind: { const: 'line', description: 'Line.' },\n x1: NUM('Start x, document pixels.'),\n y1: NUM('Start y, document pixels.'),\n x2: NUM('End x, document pixels.'),\n y2: NUM('End y, document pixels.'),\n weight: { type: 'number', minimum: 0, description: 'Thickness in pixels (default 1).' },\n startArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'Arrowhead at the start, or null.' },\n endArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'Arrowhead at the end, or null.' },\n transform: { $ref: '#/$defs/matrix' },\n },\n required: ['kind', 'x1', 'y1', 'x2', 'y2'],\n additionalProperties: false,\n },\n ],\n },\n subpaths: {\n description: 'A free path: SVG path data (absolute or relative M, L, C, Q, A, Z) or subpaths with anchors. Handles are absolute document positions.',\n anyOf: [\n { type: 'string', description: 'SVG path data, document pixels.' },\n {\n type: 'array',\n minItems: 1,\n description: 'Subpaths.',\n items: {\n type: 'object',\n description: 'One subpath.',\n properties: {\n anchors: {\n type: 'array',\n minItems: 1,\n description: 'Anchors in order.',\n items: {\n type: 'object',\n description: 'An anchor with optional handles.',\n properties: {\n x: NUM('Anchor x, document pixels.'),\n y: NUM('Anchor y, document pixels.'),\n in: { $ref: '#/$defs/point', description: 'Handle of the segment arriving here (absolute).' },\n out: { $ref: '#/$defs/point', description: 'Handle of the segment leaving here (absolute).' },\n corner: { type: 'number', minimum: 0, description: 'Live corner radius at this anchor, pixels.' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n closed: { type: 'boolean', description: 'Joins the last anchor back to the first (default true).' },\n },\n required: ['anchors'],\n additionalProperties: false,\n },\n },\n ],\n },\n gradient: {\n type: 'object',\n description: 'A gradient: colour stops, optional opacity stops and its layout (aligned with the shape bounds by default).',\n properties: {\n stops: {\n type: 'array',\n minItems: 1,\n description: 'Colour stops.',\n items: {\n type: 'object',\n description: 'A colour stop.',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, color: { type: 'string', description: 'Colour (#rrggbb or a CSS colour).' } },\n required: ['position', 'color'],\n additionalProperties: false,\n },\n },\n opacityStops: {\n type: 'array',\n description: 'Opacity stops (default opaque).',\n items: {\n type: 'object',\n description: 'An opacity stop.',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1.' } },\n required: ['position', 'opacity'],\n additionalProperties: false,\n },\n },\n style: { type: 'string', enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'], description: 'Geometry (default linear).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees, 90 = bottom to top (default 90).' },\n scale: { type: 'number', minimum: 0.1, maximum: 1.5, description: '1 = 100 % (default 1).' },\n reverse: { type: 'boolean', description: 'Swap the ends (default false).' },\n align: { type: 'boolean', description: 'Lay out over the shape bounds (true, default) or the canvas.' },\n offset: { $ref: '#/$defs/point', description: 'Centre offset as fractions of the bounds (default 0, 0).' },\n },\n required: ['stops'],\n additionalProperties: false,\n },\n paint: {\n description: 'What a fill or stroke paints with: a colour string (#rrggbb, #rrggbbaa, rgb(), a CSS name; alpha becomes opacity), or an object of type color, gradient or pattern.',\n anyOf: [\n { type: 'string', description: 'A colour.' },\n {\n type: 'object',\n description: 'Solid colour.',\n properties: { type: { const: 'color', description: 'Solid colour.' }, color: { type: 'string', description: 'Colour.' }, opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1 (default 1).' } },\n required: ['type', 'color'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Gradient.',\n properties: { type: { const: 'gradient', description: 'Gradient.' }, gradient: { $ref: '#/$defs/gradient' }, opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1 (default 1).' } },\n required: ['type', 'gradient'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Pattern (list_patterns gives ids).',\n properties: {\n type: { const: 'pattern', description: 'Pattern.' },\n patternId: { type: 'string', description: 'A document or built-in pattern id.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 % (default 1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees counter-clockwise (default 0).' },\n link: { type: 'boolean', description: 'Tile from the shape’s top-left (default true) instead of the canvas origin.' },\n phase: { $ref: '#/$defs/point', description: 'Origin offset in pixels.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1 (default 1).' },\n },\n required: ['type', 'patternId'],\n additionalProperties: false,\n },\n ],\n },\n stroke: {\n type: 'object',\n description: 'The shape stroke (not the layer-effect stroke). Unset settings keep their current values (or the defaults).',\n properties: {\n width: { type: 'number', minimum: 0, maximum: 10000, description: 'Width in pixels (required for a new stroke).' },\n paint: { $ref: '#/$defs/paint' },\n color: { type: 'string', description: 'Shorthand for paint as a colour.' },\n align: { type: 'string', enum: ['inside', 'center', 'outside'], description: 'Where it sits on the outline (default center; open paths always centre).' },\n cap: { type: 'string', enum: ['butt', 'round', 'square'], description: 'Line ends (default butt).' },\n join: { type: 'string', enum: ['miter', 'round', 'bevel'], description: 'Corners (default miter).' },\n miterLimit: { type: 'number', minimum: 1, maximum: 500, description: 'Miter length over width above which a corner is bevelled (default 100).' },\n dashes: { type: 'array', maxItems: 32, items: { type: 'number', minimum: 0, description: 'Length in multiples of the width.' }, description: 'Dash and gap lengths in multiples of the stroke width ([] = solid).' },\n dashOffset: { type: 'number', description: 'Start offset into the dashes, multiples of the width (default 0).' },\n },\n additionalProperties: false,\n },\n} as const;\n\nconst GEOMETRY = {\n shape: { $ref: '#/$defs/shape', description: 'A live shape: { kind: rect | ellipse | polygon | star | line, … } in document pixels. Give shape or subpaths.' },\n subpaths: { $ref: '#/$defs/subpaths', description: 'A free path: SVG path data or subpaths with anchors, document pixels. Give shape or subpaths.' },\n} as const;\n\nfunction geometry(args: Record<string, unknown>): ShapeGeometryInput {\n const out: ShapeGeometryInput = {};\n if (args.shape !== undefined) out.shape = args.shape as ShapeGeometryInput['shape'];\n if (args.subpaths !== undefined) out.subpaths = args.subpaths as ShapeGeometryInput['subpaths'];\n if (args.op !== undefined) out.op = args.op as ShapeGeometryInput['op'];\n return out;\n}\n\n/**\n * Agent tools for shape layers in the image editor: `get_shape_layer`, `add_shape_layer`,\n * `update_shape` (fill, stroke, fill rule), `set_shape_component`, `add_shape_component`,\n * `remove_shape_component` and `merge_shape_components`. Every write is one undo step, validates\n * first and returns the shape as it is afterwards.\n */\nexport function createImageShapeTools(controller: ImageEditorController): AgentTool[] {\n const shapeId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a shape layer id from list_layers).');\n requireShapeLayer(controller.getLayers(), id);\n return id;\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 get: AgentTool = {\n name: 'get_shape_layer',\n title: 'Read a shape layer',\n description:\n 'Returns a shape layer’s components bottom to top (index, path operation, the live shape it was drawn as or null, its subpaths with absolute anchors and handles, bounds), the fill rule, fill and stroke paints and settings, the path bounds and the rendered bounds (stroke included). Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a shape layer', ...READ },\n handler: ({ layerId }) => controller.getShapeLayer(shapeId(layerId)),\n };\n\n const add: AgentTool = {\n name: 'add_shape_layer',\n title: 'Add a shape layer',\n description:\n 'Adds a vector shape layer and makes it active: a live shape (rect, ellipse, polygon, star, line), a free path (SVG path data or anchors), or several components combined with path operations. fill defaults to the foreground colour (none for an open path with a stroke); give stroke for an outline. It renders sharp at any size, can be moved and transformed without losing quality, and takes masks, effects, clipping and blend modes like any layer. Goes above the active layer unless placed. One undo step. Returns the layer and the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n ...GEOMETRY,\n components: {\n type: 'array',\n minItems: 1,\n description: 'Several components instead of shape / subpaths, bottom first, each with its path operation.',\n items: { type: 'object', description: 'One component.', properties: { ...GEOMETRY, op: OP }, additionalProperties: false },\n },\n fill: { anyOf: [{ $ref: '#/$defs/paint' }, { type: 'null', description: 'No fill.' }], description: 'Fill paint, or null for none (default the foreground colour).' },\n stroke: { anyOf: [{ $ref: '#/$defs/stroke' }, { type: 'null', description: 'No stroke.' }], description: 'Stroke { width, paint, … }, or null / omitted for none.' },\n fillRule: { type: 'string', enum: ['nonzero', 'evenodd'], description: 'Winding rule within a component (default nonzero).' },\n name: { type: 'string', description: 'Layer name (default by kind: Rectangle, Ellipse, …).' },\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 aboveId: { type: 'string', description: 'Put the layer directly above this layer (default above the active layer).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer.' },\n parentId: { type: 'string', description: 'Put the layer inside this group.' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a shape layer', ...WRITE },\n handler: (args) => {\n const init: ShapeLayerInit = { ...geometry(args) };\n if (args.components !== undefined) init.components = (args.components as Array<Record<string, unknown>>).map(geometry);\n for (const k of ['fill', 'stroke', 'fillRule', 'name', 'opacity', 'blend', 'aboveId', 'belowId', 'parentId', 'index'] as const) {\n if (args[k] !== undefined) (init as Record<string, unknown>)[k] = args[k];\n }\n if (init.blend !== undefined) init.blend = init.blend as BlendMode;\n const id = controller.addShapeLayer(init);\n return { layer: layerSummary(id), shape: controller.getShapeLayer(id) };\n },\n };\n\n const update: AgentTool = {\n name: 'update_shape',\n title: 'Restyle a shape',\n description:\n 'Changes a shape layer’s fill, stroke or fill rule and renders it again: fill replaces the fill (null removes it); stroke changes only the settings given (null removes the stroke; a new stroke needs width and paint). Geometry is untouched (set_shape_component edits paths). One undo step. Returns the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n fill: { anyOf: [{ $ref: '#/$defs/paint' }, { type: 'null', description: 'No fill.' }], description: 'New fill paint, or null for none.' },\n stroke: { anyOf: [{ $ref: '#/$defs/stroke' }, { type: 'null', description: 'No stroke.' }], description: 'Stroke settings to change, or null to remove the stroke.' },\n fillRule: { type: 'string', enum: ['nonzero', 'evenodd'], description: 'Winding rule within a component.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Restyle a shape', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, fill, stroke, fillRule }) => {\n const id = shapeId(layerId);\n if (fill === undefined && stroke === undefined && fillRule === undefined) throw new LayerOperationError('Give fill, stroke or fillRule to change.');\n return controller.setShapeStyle(id, { fill: fill as never, stroke: stroke as never, fillRule: fillRule as never });\n },\n };\n\n const setComponent: AgentTool = {\n name: 'set_shape_component',\n title: 'Edit a shape component',\n description:\n 'Changes one component of a shape layer: shape sets its live shape (fields you leave out keep their values when the kind stays the same, e.g. only radii), subpaths replaces its path (which ends the live shape), op changes its path operation. Positions are document pixels. One undo step. Returns the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n componentIndex: COMPONENT_INDEX,\n shape: {\n type: 'object',\n description: 'Live shape fields to set (see add_shape_layer’s shape): kind plus its fields; without kind, the fields change the current live shape.',\n properties: {\n kind: { type: 'string', enum: ['rect', 'ellipse', 'polygon', 'star', 'line'], description: 'Shape kind (default the current one).' },\n x: NUM('Left of the box, pixels.'),\n y: NUM('Top of the box, pixels.'),\n width: NUM('Box width, pixels.'),\n height: NUM('Box height, pixels.'),\n radius: { type: 'number', minimum: 0, description: 'rect: one corner radius, pixels.' },\n radii: { type: 'array', items: { type: 'number', minimum: 0, description: 'Radius in pixels.' }, minItems: 4, maxItems: 4, description: 'rect: [top-left, top-right, bottom-right, bottom-left] radii, pixels.' },\n sides: { type: 'integer', minimum: 3, maximum: 100, description: 'polygon: number of sides.' },\n points: { type: 'integer', minimum: 3, maximum: 100, description: 'star: number of points.' },\n innerRatio: { type: 'number', minimum: 0.01, maximum: 1, description: 'star: inner over outer radius.' },\n x1: NUM('line: start x.'),\n y1: NUM('line: start y.'),\n x2: NUM('line: end x.'),\n y2: NUM('line: end y.'),\n weight: { type: 'number', minimum: 0, description: 'line: thickness in pixels.' },\n startArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'line: start arrowhead or null.' },\n endArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'line: end arrowhead or null.' },\n transform: { $ref: '#/$defs/matrix' },\n },\n additionalProperties: false,\n },\n subpaths: { $ref: '#/$defs/subpaths', description: 'A new path for the component (SVG path data or anchors); ends its live shape.' },\n op: OP,\n },\n required: ['layerId', 'componentIndex'],\n additionalProperties: false,\n },\n annotations: { title: 'Edit a shape component', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, componentIndex, ...rest }) => {\n const id = shapeId(layerId);\n const change = geometry(rest);\n if (change.shape === undefined && change.subpaths === undefined && change.op === undefined) throw new LayerOperationError('Give shape, subpaths or op to change.');\n return controller.setShapeComponent(id, componentIndex as number, change);\n },\n };\n\n const addComponent: AgentTool = {\n name: 'add_shape_component',\n title: 'Add a shape component',\n description:\n 'Adds a component to a shape layer and combines it with the components below it by op: combine (union), subtract, intersect or exclude, as Photoshop’s path operations. Goes on top unless index is given. One undo step. Returns the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: { layerId: LAYER_ID, ...GEOMETRY, op: OP, index: { type: 'integer', minimum: 0, description: 'Position among the components, 0 = bottom (default the top).' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a shape component', ...WRITE },\n handler: ({ layerId, index, ...rest }) => controller.addShapeComponent(shapeId(layerId), geometry(rest), index as number | undefined),\n };\n\n const removeComponent: AgentTool = {\n name: 'remove_shape_component',\n title: 'Delete a shape component',\n description: 'Deletes one component of a shape layer (the last one cannot go: delete the layer instead). One undo step. Returns the shape.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, componentIndex: COMPONENT_INDEX }, required: ['layerId', 'componentIndex'], additionalProperties: false },\n annotations: { title: 'Delete a shape component', ...DESTRUCTIVE },\n handler: ({ layerId, componentIndex }) => controller.removeShapeComponent(shapeId(layerId), componentIndex as number),\n };\n\n const merge: AgentTool = {\n name: 'merge_shape_components',\n title: 'Merge shape components',\n description:\n 'Merge Shape Components: the area a shape’s components combine to (after their path operations) becomes one plain component; live shapes become paths. The look is unchanged. One undo step. Returns the shape.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge shape components', ...DESTRUCTIVE },\n handler: ({ layerId }) => controller.mergeShapeComponents(shapeId(layerId)),\n };\n\n return [get, add, update, setComponent, addComponent, removeComponent, merge];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController, TextLayerInit } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError } from './layers/ops';\nimport { requireTypeLayer } from './layers/text';\nimport { describeLayer } 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 LAYER_ID = { type: 'string', description: 'Type layer id from list_layers (type type) or add_text_layer.' } as const;\nconst START = { type: 'integer', minimum: 0, description: 'Start of the range in UTF-16 code units, as get_text_layer counts them (default 0).' } as const;\nconst END = { type: 'integer', minimum: 0, description: 'End of the range, exclusive (default the end of the text).' } as const;\n\nconst STYLE = {\n fontFamily: { type: 'string', description: 'Font family name (list_fonts shows what is available; a missing family falls back, with a warning).' },\n fontWeight: { type: 'integer', minimum: 1, maximum: 1000, description: 'Weight 100..900 (400 regular, 700 bold).' },\n italic: { type: 'boolean', description: 'Italic.' },\n fontSize: { type: 'number', minimum: 0.5, maximum: 5000, description: 'Size in document pixels.' },\n color: { type: 'string', description: 'Colour (#rrggbb, or a CSS colour; its alpha becomes opacity).' },\n opacity: { type: ['number', 'null'], minimum: 0, maximum: 1, description: 'Text opacity 0..1 (null resets to 1).' },\n tracking: { type: ['number', 'null'], minimum: -1000, maximum: 10000, description: 'Letter spacing in 1/1000 em (null resets to 0).' },\n leading: { type: ['number', 'null'], minimum: 0, maximum: 10000, description: 'Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading × size).' },\n baselineShift: { type: ['number', 'null'], minimum: -5000, maximum: 5000, description: 'Pixels; positive raises the text (null resets to 0).' },\n underline: { type: 'boolean', description: 'Underline.' },\n strikethrough: { type: 'boolean', description: 'Strike through.' },\n} as const;\n\nconst PARAGRAPH = {\n align: { type: 'string', enum: ['left', 'center', 'right', 'justify'], description: 'Alignment (justify stretches paragraph-box lines but the last).' },\n autoLeading: { type: ['number', 'null'], minimum: 0.1, maximum: 10, description: 'Auto leading as a multiple of the font size (default 1.2).' },\n firstLineIndent: { type: ['number', 'null'], description: 'First-line indent in pixels.' },\n startIndent: { type: ['number', 'null'], description: 'Left indent in pixels.' },\n endIndent: { type: ['number', 'null'], description: 'Right indent in pixels.' },\n spaceBefore: { type: ['number', 'null'], description: 'Space above the paragraph in pixels (not before the first).' },\n spaceAfter: { type: ['number', 'null'], description: 'Space below the paragraph in pixels.' },\n} as const;\n\nfunction pick(args: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const k of keys) if (args[k] !== undefined) out[k] = args[k];\n return out;\n}\n\n/**\n * Agent tools for type layers in the image editor: `list_fonts`, `get_text_layer`,\n * `add_text_layer`, `set_text`, `set_text_style`, `set_paragraph_style`, `set_text_frame` and\n * `convert_text_to_shape`. Every write is one undo step, validates first and returns the layer as\n * it is afterwards (with any missing fonts).\n */\nexport function createImageTypeTools(controller: ImageEditorController): AgentTool[] {\n const typeId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a type layer id from list_layers).');\n requireTypeLayer(controller.getLayers(), id);\n return id;\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 const range = (args: Record<string, unknown>) => ({ start: args.start as number | undefined, end: args.end as number | undefined });\n\n const listFonts: AgentTool = {\n name: 'list_fonts',\n title: 'List fonts',\n description: 'Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List fonts', ...READ },\n handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null }),\n };\n\n const get: AgentTool = {\n name: 'get_text_layer',\n title: 'Read a type layer',\n description:\n 'Returns a type layer’s text, its character runs and paragraphs as [start, end) ranges in UTF-16 code units with their styles, the frame (point text, or a paragraph box with its size), the text-to-document transform, the laid-out lines (range, baseline, width), the rendered bounds, missing fonts and whether the pixels are live or stored from a file. Call before editing text you did not write, to get the ranges. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a type layer', ...READ },\n handler: ({ layerId }) => controller.getTextLayer(typeId(layerId)),\n };\n\n const add: AgentTool = {\n name: 'add_text_layer',\n title: 'Add a type layer',\n description:\n 'Adds a live type layer and makes it active: point text whose origin (x, y) is the first line’s baseline at its alignment edge, or paragraph text wrapped in box {width, height} with its top-left at (x, y). Line breaks in text start paragraphs. Style and paragraph settings apply to the whole text (change ranges later with set_text_style). Default style: 24 px sans-serif in the foreground colour. One undo step. Returns the layer and the text layer, including missingFonts.',\n inputSchema: {\n type: 'object',\n properties: {\n text: { type: 'string', description: 'The text; \\\\n starts a new paragraph.' },\n x: { type: 'number', description: 'Document x of the origin, pixels.' },\n y: { type: 'number', description: 'Document y of the origin (point text: the first baseline), pixels.' },\n box: {\n type: 'object',\n description: 'Make it paragraph (area) text wrapped in this box, pixels.',\n properties: { width: { type: 'number', minimum: 1, description: 'Box width, pixels.' }, height: { type: 'number', minimum: 1, description: 'Box height, pixels.' } },\n required: ['width', 'height'],\n additionalProperties: false,\n },\n style: { type: 'object', description: 'Character style.', properties: STYLE, additionalProperties: false },\n paragraph: { type: 'object', description: 'Paragraph style.', properties: PARAGRAPH, additionalProperties: false },\n rotation: { type: 'number', description: 'Degrees clockwise about the origin (default 0).' },\n name: { type: 'string', description: 'Layer name (default the start of the text).' },\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 aboveId: { type: 'string', description: 'Put the layer directly above this layer (default above the active layer).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer.' },\n parentId: { type: 'string', description: 'Put the layer inside this group.' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom.' },\n },\n required: ['text', 'x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a type layer', ...WRITE },\n handler: (args) => {\n const init = pick(args, ['text', 'x', 'y', 'box', 'style', 'paragraph', 'rotation', 'name', 'opacity', 'blend', 'aboveId', 'belowId', 'parentId', 'index']) as unknown as TextLayerInit;\n if (init.blend !== undefined) init.blend = init.blend as BlendMode;\n const id = controller.addTextLayer(init);\n return { layer: layerSummary(id), text: controller.getTextLayer(id) };\n },\n };\n\n const setText: AgentTool = {\n name: 'set_text',\n title: 'Edit text',\n description:\n 'Replaces the text in [start, end) (default all of it) with text; inserted text takes the style of the character at start. Use start = end to insert. Returns the text layer with its new runs.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, text: { type: 'string', description: 'Replacement text (\\\\n starts a paragraph; empty deletes the range).' }, start: START, end: END }, required: ['layerId', 'text'], additionalProperties: false },\n annotations: { title: 'Edit text', ...WRITE },\n handler: (args) => controller.setText(typeId(args.layerId), args.text as string, range(args)),\n };\n\n const setStyle: AgentTool = {\n name: 'set_text_style',\n title: 'Style a range of text',\n description:\n 'Sets character style fields (font, weight, italic, size, colour, tracking, leading, baseline shift, underline, strike-through) over [start, end) of a type layer (default all of it); fields not given keep their values per run. Returns the text layer with its runs and missingFonts.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, start: START, end: END, ...STYLE }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Style a range of text', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const style = pick(args, Object.keys(STYLE));\n if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(', ')}.`);\n return controller.setTextStyle(typeId(args.layerId), style, range(args));\n },\n };\n\n const setParagraph: AgentTool = {\n name: 'set_paragraph_style',\n title: 'Style paragraphs',\n description: 'Sets paragraph fields (alignment, auto leading, indents, spacing) on every paragraph that [start, end) touches (default all). Returns the text layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, start: START, end: END, ...PARAGRAPH }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Style paragraphs', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const style = pick(args, Object.keys(PARAGRAPH));\n if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(', ')}.`);\n return controller.setParagraphStyle(typeId(args.layerId), style, range(args));\n },\n };\n\n const setFrame: AgentTool = {\n name: 'set_text_frame',\n title: 'Point or paragraph text',\n description:\n 'Converts between point text (kind point: one line per paragraph) and paragraph text (kind box: wrapped in width × height pixels, its top-left where the first line starts), or resizes a paragraph box. The first line stays in place. Returns the text layer with its new lines.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: ['point', 'box'], description: 'point or box.' },\n width: { type: 'number', minimum: 1, description: 'Box width in pixels (kind box).' },\n height: { type: 'number', minimum: 1, description: 'Box height in pixels (kind box).' },\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Point or paragraph text', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, kind, width, height }) => {\n const id = typeId(layerId);\n if (kind === 'box' && (width === undefined || height === undefined)) throw new LayerOperationError('kind box needs width and height in pixels.');\n return controller.setTextFrame(id, kind === 'box' ? { kind: 'box', width: width as number, height: height as number } : { kind: 'point' });\n },\n };\n\n const toShape: AgentTool = {\n name: 'convert_text_to_shape',\n title: 'Convert text to a shape',\n description:\n 'Type › Convert to Shape: the text’s glyph outlines become a shape layer (same id, name, mask, effects, opacity, blend) that can be edited with the shape tools; the text is no longer editable. Needs a font shaper that gives outlines (the browser’s canvas cannot). The fill is the first run’s colour. One undo step. Returns the layer and warnings.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Convert text to a shape', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const { layerId: id, warnings } = controller.convertTextToShape(typeId(layerId));\n return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };\n },\n };\n\n return [listFonts, get, add, setText, setStyle, setParagraph, setFrame, toShape];\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 { createImageRetouchTools } from './retouch-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\nimport { createImageTransformTools } from './transform-tools';\nimport { createImageSmartObjectTools } from './smart-tools';\nimport { createImageShapeTools } from './shape-tools';\nimport { createImageTypeTools } from './type-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 * - Shape layers: `get_shape_layer`, `add_shape_layer`, `update_shape`, `set_shape_component`,\n * `add_shape_component`, `remove_shape_component`, `merge_shape_components` (see\n * `createImageShapeTools`); `rasterize_layer` turns one into pixels.\n * - Type layers: `list_fonts`, `get_text_layer`, `add_text_layer`, `set_text`, `set_text_style`,\n * `set_paragraph_style`, `set_text_frame`, `convert_text_to_shape` (see `createImageTypeTools`).\n * - Retouching: `clone_stamp`, `heal_brush`, `spot_heal`, `smudge`, `dodge`, `burn`, `sponge`,\n * `get_retouch_options`, `set_retouch_options`, `set_clone_source` (see `createImageRetouchTools`).\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 ...createImageShapeTools(controller),\n ...createImageTypeTools(controller),\n ...createImageEffectTools(controller),\n ...createImagePaintTools(controller),\n ...createImageRetouchTools(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 * Framework-free logic behind the shape and type tools of the React and Svelte image editors:\n * tool options, a drag that draws a live shape (one undo step, the layer updated live while\n * dragging), and on-canvas text editing (caret, selection, typing, keys, styling the selection,\n * the overlay to draw). The components only translate DOM events and draw `overlay()`.\n */\n\nimport type { ImageEditorController, TypeLayerInfo } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { applyMatrix } from '../../geom/affine';\nimport { DEFAULT_TEXT_STYLE, caretAt, styleAt, textMatrix, typeLayerName, type TextLayout } from './text';\nimport { findLayer, type LiveShapeKind, type ParagraphStyle, type TextAlign, type TextStyle, type TypeLayer } from './model';\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/** What the shape tool draws next (and what its options bar edits on a selected shape). */\nexport interface ShapeToolOptions {\n kind: LiveShapeKind;\n /** `#rrggbb`, or `null` for no fill. */\n fill: string | null;\n /** `#rrggbb`, or `null` for no stroke. */\n stroke: string | null;\n strokeWidth: number;\n /** Rectangles: corner radius in pixels. */\n radius: number;\n /** Polygons: sides. */\n sides: number;\n /** Stars: points and inner radius ratio. */\n points: number;\n innerRatio: number;\n /** Lines: thickness in pixels and whether to add an arrowhead at the end. */\n weight: number;\n arrow: boolean;\n}\n\nexport const DEFAULT_SHAPE_TOOL_OPTIONS: ShapeToolOptions = {\n kind: 'rect',\n fill: '#4f46e5',\n stroke: null,\n strokeWidth: 2,\n radius: 0,\n sides: 6,\n points: 5,\n innerRatio: 0.5,\n weight: 4,\n arrow: false,\n};\n\nexport const SHAPE_KIND_LABELS: Record<LiveShapeKind, string> = { rect: 'Rectangle', ellipse: 'Ellipse', polygon: 'Polygon', star: 'Star', line: 'Line' };\n\n/** What the type tool sets new text in (and what its options bar edits on the selection). */\nexport interface TypeToolOptions {\n fontFamily: string;\n fontSize: number;\n fontWeight: number;\n italic: boolean;\n color: string;\n align: TextAlign;\n}\n\nexport const DEFAULT_TYPE_TOOL_OPTIONS: TypeToolOptions = { fontFamily: 'sans-serif', fontSize: 32, fontWeight: 400, italic: false, color: '#111827', align: 'left' };\n\n/** A live shape for a drag from `a` to `b` (Shift: square / circle / 45° line; Alt: from the centre). */\nexport function liveShapeForDrag(o: ShapeToolOptions, a: ImagePoint, b: ImagePoint, mods: { shift?: boolean; alt?: boolean } = {}): Record<string, unknown> | null {\n if (o.kind === 'line') {\n let x2 = b.x;\n let y2 = b.y;\n if (mods.shift) {\n const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n x2 = a.x + Math.cos(ang) * len;\n y2 = a.y + Math.sin(ang) * len;\n }\n if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;\n const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};\n return { kind: 'line', x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };\n }\n let w = b.x - a.x;\n let h = b.y - a.y;\n if (mods.shift) {\n const s = Math.max(Math.abs(w), Math.abs(h));\n w = Math.sign(w || 1) * s;\n h = Math.sign(h || 1) * s;\n }\n let x = a.x;\n let y = a.y;\n if (mods.alt) {\n x = a.x - w;\n y = a.y - h;\n w *= 2;\n h *= 2;\n }\n const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };\n if (box.width < 1 || box.height < 1) return null;\n switch (o.kind) {\n case 'rect':\n return { kind: 'rect', ...box, ...(o.radius > 0 ? { radius: o.radius } : {}) };\n case 'ellipse':\n return { kind: 'ellipse', ...box };\n case 'polygon':\n return { kind: 'polygon', ...box, sides: o.sides };\n case 'star':\n return { kind: 'star', ...box, points: o.points, innerRatio: o.innerRatio };\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Shape drawing\n// ---------------------------------------------------------------------------\n\n/**\n * A drag that draws a live shape: the layer is added on the first real move (one undo step) and\n * its component updated live (no history) until the pointer is released.\n */\nexport class ShapeDrawGesture {\n private start: ImagePoint | null = null;\n private layerId: string | null = null;\n error: string | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: () => ShapeToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The shape layer drawn by the last drag (until the next one starts). */\n get drawnLayerId(): string | null {\n return this.layerId;\n }\n\n down(p: ImagePoint): void {\n this.error = null;\n this.start = { x: p.x, y: p.y };\n this.layerId = null;\n }\n\n move(p: ImagePoint, mods: { shift?: boolean; alt?: boolean } = {}): void {\n if (!this.start) return;\n const o = this.options();\n const shape = liveShapeForDrag(o, this.start, p, mods);\n if (!shape) return;\n try {\n if (!this.layerId) {\n const isLine = o.kind === 'line';\n this.layerId = this.controller.addShapeLayer({\n shape,\n fill: isLine ? (o.stroke ?? o.fill ?? '#000000') : o.fill,\n stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null,\n });\n } else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n this.start = null;\n }\n }\n\n up(): string | null {\n this.start = null;\n return this.layerId;\n }\n\n cancel(): void {\n this.start = null;\n }\n}\n\n/** Applies options-bar changes to a shape layer (fill, stroke, width, and the live shape's own fields). */\nexport function applyShapeOptions(controller: ImageEditorController, layerId: string, change: Partial<ShapeToolOptions>): void {\n const info = controller.getShapeLayer(layerId);\n const style: Parameters<ImageEditorController['setShapeStyle']>[1] = {};\n if ('fill' in change) style.fill = change.fill ?? null;\n if ('stroke' in change || 'strokeWidth' in change) {\n const color = 'stroke' in change ? change.stroke : (info.stroke?.paint.type === 'color' ? info.stroke.paint.color : null);\n style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;\n }\n if (Object.keys(style).length) controller.setShapeStyle(layerId, style);\n const live = info.components[0]?.live;\n if (!live) return;\n const shape: Record<string, unknown> = {};\n if (live.kind === 'rect' && change.radius !== undefined) shape.radius = change.radius;\n if (live.kind === 'polygon' && change.sides !== undefined) shape.sides = change.sides;\n if (live.kind === 'star' && change.points !== undefined) shape.points = change.points;\n if (live.kind === 'star' && change.innerRatio !== undefined) shape.innerRatio = change.innerRatio;\n if (live.kind === 'line' && change.weight !== undefined) shape.weight = change.weight;\n if (live.kind === 'line' && change.arrow !== undefined) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;\n if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });\n}\n\n// ---------------------------------------------------------------------------\n// Text editing\n// ---------------------------------------------------------------------------\n\n/** The keys a text session understands, as DOM `KeyboardEvent`s report them. */\nexport interface TextKey {\n key: string;\n shiftKey?: boolean;\n metaKey?: boolean;\n ctrlKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Editing one type layer on the canvas: a caret and a selection (`anchor`..`focus`, UTF-16 code\n * units), typing, deleting, moving, clicking and styling the selection. The first change of a\n * session records one undo step; the rest join it.\n */\nexport class TextEditSession {\n anchor: number;\n focus: number;\n private changed = false;\n private readonly listeners = new Set<() => void>();\n error: string | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly layerId: string,\n index?: number\n ) {\n const n = this.text.length;\n this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));\n }\n\n private get layer(): TypeLayer {\n const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;\n if (!l || l.type !== 'type') throw new Error('The type layer being edited is gone.');\n return l;\n }\n\n get text(): string {\n return this.layer.text;\n }\n\n get start(): number {\n return Math.min(this.anchor, this.focus);\n }\n\n get end(): number {\n return Math.max(this.anchor, this.focus);\n }\n\n /** Whether the layer still exists (it can be deleted or undone away while editing). */\n get alive(): boolean {\n return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === 'type';\n }\n\n subscribe(fn: () => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private emit(): void {\n for (const l of this.listeners) l();\n }\n\n private layout(): TextLayout | null {\n return this.controller.getTextLayout(this.layerId);\n }\n\n private run<T>(fn: (commit: boolean) => T): T | undefined {\n this.error = null;\n try {\n const out = fn(!this.changed);\n this.changed = true;\n return out;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n return undefined;\n } finally {\n this.emit();\n }\n }\n\n /** Replaces the selection with `text` (typing and pasting). */\n insert(text: string): void {\n if (!text) return;\n const s = this.start;\n const e = this.end;\n const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));\n if (done) this.anchor = this.focus = s + text.replace(/\\r\\n?/g, '\\n').length;\n }\n\n /** Backspace: the selection, else the character before the caret. */\n deleteBackward(): void {\n if (this.start === this.end && this.start === 0) return;\n const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;\n const e = this.end;\n if (this.run((commit) => this.controller.setText(this.layerId, '', { start: s, end: e }, commit))) this.anchor = this.focus = s;\n }\n\n /** Delete: the selection, else the character after the caret. */\n deleteForward(): void {\n const n = this.text.length;\n if (this.start === this.end && this.end >= n) return;\n const s = this.start;\n const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;\n if (this.run((commit) => this.controller.setText(this.layerId, '', { start: s, end: e }, commit))) this.anchor = this.focus = s;\n }\n\n /** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */\n private surrogate(i: number, dir: 1 | -1): number {\n const t = this.text;\n const c = t.charCodeAt(i);\n if (dir < 0 && c >= 0xdc00 && c <= 0xdfff && i > 0) return 2;\n if (dir > 0 && c >= 0xd800 && c <= 0xdbff && i + 1 < t.length) return 2;\n return 1;\n }\n\n /** Moves the caret (Shift extends the selection). */\n move(to: 'left' | 'right' | 'up' | 'down' | 'home' | 'end' | 'start' | 'finish', extend = false): void {\n const n = this.text.length;\n let f = this.focus;\n const collapsedJump = !extend && this.start !== this.end;\n if (to === 'left') f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));\n else if (to === 'right') f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));\n else if (to === 'start') f = 0;\n else if (to === 'finish') f = n;\n else {\n const layout = this.layout();\n if (layout && layout.lines.length) {\n const c = caretAt(layout, f);\n const line = layout.lines[c.line];\n if (to === 'home') f = line.start;\n else if (to === 'end') {\n const last = line.chars[line.chars.length - 1];\n f = last ? Math.min(n, last.index + 1) : line.start;\n if (last && this.text[last.index] === ' ' && c.line < layout.lines.length - 1) f = last.index;\n } else {\n const target = layout.lines[c.line + (to === 'up' ? -1 : 1)];\n if (!target) f = to === 'up' ? 0 : n;\n else {\n const hit = target.chars.find((ch) => c.x < ch.x + ch.advance / 2);\n const last = target.chars[target.chars.length - 1];\n f = hit ? hit.index : last ? last.index + 1 : target.start;\n }\n }\n }\n }\n this.focus = f;\n if (!extend) this.anchor = f;\n this.emit();\n }\n\n selectAll(): void {\n this.anchor = 0;\n this.focus = this.text.length;\n this.emit();\n }\n\n /** Places the caret at a document point (Shift-click extends). */\n pointAt(p: ImagePoint, extend = false): void {\n this.focus = this.controller.textIndexAt(this.layerId, p);\n if (!extend) this.anchor = this.focus;\n this.emit();\n }\n\n /** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */\n handleKey(e: TextKey): boolean {\n const mod = e.metaKey || e.ctrlKey;\n if (mod && e.key.toLowerCase() === 'a') {\n this.selectAll();\n return true;\n }\n switch (e.key) {\n case 'Backspace':\n this.deleteBackward();\n return true;\n case 'Delete':\n this.deleteForward();\n return true;\n case 'Enter':\n this.insert('\\n');\n return true;\n case 'ArrowLeft':\n this.move(mod ? 'home' : 'left', e.shiftKey);\n return true;\n case 'ArrowRight':\n this.move(mod ? 'end' : 'right', e.shiftKey);\n return true;\n case 'ArrowUp':\n this.move(mod ? 'start' : 'up', e.shiftKey);\n return true;\n case 'ArrowDown':\n this.move(mod ? 'finish' : 'down', e.shiftKey);\n return true;\n case 'Home':\n this.move('home', e.shiftKey);\n return true;\n case 'End':\n this.move('end', e.shiftKey);\n return true;\n case 'Tab':\n this.insert('\\t');\n return true;\n }\n if (!mod && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {\n this.insert(e.key);\n return true;\n }\n return false;\n }\n\n /** Sets character style fields on the selection, or on the whole text when nothing is selected. */\n setStyle(style: Partial<TextStyle>): void {\n const s = this.start;\n const e = this.end;\n const range = s === e ? {} : { start: s, end: e };\n this.run((commit) => this.controller.setTextStyle(this.layerId, style as Partial<TextStyle> & Record<string, unknown>, range, commit));\n }\n\n /** Sets paragraph fields on the paragraphs the selection touches. */\n setParagraph(style: ParagraphStyle): void {\n const s = this.start;\n const e = this.end;\n this.run((commit) => this.controller.setParagraphStyle(this.layerId, style as ParagraphStyle & Record<string, unknown>, { start: s, end: e }, commit));\n }\n\n /** The style at the caret (or the selection's start), for the options bar. */\n styleHere(): TextStyle {\n const layer = this.layer;\n const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;\n return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);\n }\n\n info(): TypeLayerInfo {\n return this.controller.getTextLayer(this.layerId);\n }\n\n /** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */\n overlay(): { caret: [ImagePoint, ImagePoint] | null; selection: ImagePoint[][]; frame: ImagePoint[] | null } {\n const layer = this.layer;\n const layout = this.layout();\n const m = textMatrix(layer);\n const at = (x: number, y: number) => applyMatrix(m, { x, y });\n let frame: ImagePoint[] | null = null;\n if (layer.frame.kind === 'box') frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];\n else if (layout) {\n const b = layout.bounds;\n frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];\n }\n if (!layout) return { caret: null, selection: [], frame };\n const selection: ImagePoint[][] = [];\n if (this.start !== this.end) {\n for (const l of layout.lines) {\n const chars = l.chars.filter((c) => c.index >= this.start && c.index < this.end);\n if (!chars.length) continue;\n const x0 = chars[0].x;\n const x1 = chars[chars.length - 1].x + chars[chars.length - 1].advance;\n const top = l.baseline - l.ascent;\n const bottom = l.baseline + l.descent;\n selection.push([at(x0, top), at(x1, top), at(x1, bottom), at(x0, bottom)]);\n }\n }\n const c = caretAt(layout, this.focus);\n return { caret: [at(c.x, c.top), at(c.x, c.bottom)], selection, frame };\n }\n\n /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */\n close(): boolean {\n if (!this.alive) return false;\n if (!this.text.trim().length) {\n try {\n this.controller.removeLayer(this.layerId);\n } catch {\n // A locked or already removed layer stays as it is.\n }\n return false;\n }\n // New text is named after what was typed, as in Photoshop.\n if (this.layer.name === 'Text' && this.changed) {\n try {\n this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);\n } catch {\n // Keep the name.\n }\n }\n return true;\n }\n}\n\n/** Where a click lands on type layers: the topmost visible type layer whose laid-out text or box contains `p`, or `null`. */\nexport function typeLayerAt(controller: ImageEditorController, p: ImagePoint, slop = 4): string | null {\n const layers = [...flattenTypeLayers(controller.getLayers())].reverse();\n for (const l of layers) {\n if (!l.visible) continue;\n const inv = invert(textMatrix(l));\n const q = applyMatrix(inv, p);\n let b: { x: number; y: number; width: number; height: number } | null = null;\n if (l.frame.kind === 'box') b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };\n else {\n const layout = controller.getTextLayout(l.id);\n if (layout) b = layout.bounds;\n }\n if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;\n }\n return null;\n}\n\nfunction flattenTypeLayers(layers: ReturnType<ImageEditorController['getLayers']>): TypeLayer[] {\n const out: TypeLayer[] = [];\n const walk = (list: typeof layers) => {\n for (const l of list) {\n if (l.type === 'type') out.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n return out;\n}\n\nfunction invert(m: readonly number[]): [number, number, number, number, number, number] {\n const [a, b, c, d, e, f] = m;\n const det = a * d - b * c || 1;\n return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];\n}\n\n/**\n * The type tool: a click on type edits it at the clicked character; a click elsewhere adds point\n * text there (a drag adds paragraph text in the dragged box). Holds the open `TextEditSession`.\n */\nexport class TypeToolGesture {\n session: TextEditSession | null = null;\n private dragStart: ImagePoint | null = null;\n error: string | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: () => TypeToolOptions\n ) {}\n\n down(p: ImagePoint, mods: { shift?: boolean } = {}): void {\n this.error = null;\n const hit = typeLayerAt(this.controller, p);\n if (this.session && this.session.alive && hit === this.session.layerId) {\n this.session.pointAt(p, mods.shift);\n this.dragStart = null;\n return;\n }\n this.finish();\n if (hit) {\n this.controller.setActiveLayer(hit);\n this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));\n this.dragStart = null;\n return;\n }\n this.dragStart = { x: p.x, y: p.y };\n }\n\n /** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */\n up(p: ImagePoint): TextEditSession | null {\n const start = this.dragStart;\n this.dragStart = null;\n if (!start) return this.session;\n const o = this.options();\n const w = Math.abs(p.x - start.x);\n const h = Math.abs(p.y - start.y);\n try {\n const box = w >= 8 && h >= 8 ? { width: w, height: h } : undefined;\n const id = this.controller.addTextLayer({\n text: '',\n x: box ? Math.min(start.x, p.x) : start.x,\n y: box ? Math.min(start.y, p.y) : start.y,\n ...(box ? { box } : {}),\n style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },\n paragraph: o.align !== 'left' ? { align: o.align } : {},\n name: 'Text',\n });\n this.session = new TextEditSession(this.controller, id, 0);\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n }\n return this.session;\n }\n\n /** Ends the open session (an empty new layer is removed). */\n finish(): void {\n const s = this.session;\n this.session = null;\n s?.close();\n }\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 case 'shape':\n return 'shp';\n case 'type':\n return 'T';\n }\n}\n\n/** Whether the layer's row shows a pixel thumbnail (pixel, shape and type layers; shapes and type with a badge on it). */\nexport function layerHasThumbnail(layer: ImageLayer): boolean {\n return layer.type === 'pixel' || layer.type === 'shape' || layer.type === 'type';\n}\n\n/** A warning to show on a layer's row (a type layer drawn with fallback fonts), or `null`. */\nexport function layerWarning(layer: ImageLayer): string | null {\n if (layer.type === 'type' && layer.missingFonts?.length) {\n return `Missing ${layer.missingFonts.length === 1 ? 'font' : 'fonts'}: ${layer.missingFonts.join(', ')}. ${layer.rendering === 'stored' ? 'Showing the saved pixels until the text is edited.' : 'Drawn with a fallback font.'}`;\n }\n return null;\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' || active?.type === 'shape' || active?.type === 'type' || (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\nexport interface NormalizedLayerItem {\n id: string;\n name: string;\n imageUrl: string;\n thumbnailUrl?: string;\n visible: boolean;\n opacity: number;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Extracts a flat list of normalized layer metadata (0..1 bounds relative to document dimensions\n * and resolved PNG data-URL thumbnails) from the controller's current V3 layer state.\n */\nexport function extractNormalizedLayerItems(\n controller: ImageEditorController,\n fallbackImageUrl = ''\n): NormalizedLayerItem[] {\n const state = controller.getState();\n const ctrlLayers = controller.getLayers();\n if (!ctrlLayers || ctrlLayers.length === 0) return [];\n\n const serialized = controller.toJSON() as {\n assets?: Record<string, string>;\n layers?: Array<{ id: string; asset?: string; source?: string; children?: unknown[] }>;\n };\n const assets = serialized.assets ?? {};\n const allSerializedLayers: Array<{ id: string; asset?: string; source?: string; children?: unknown[] }> = [];\n\n const collectSerialized = (list: Array<{ id: string; asset?: string; source?: string; children?: unknown[] }>) => {\n for (const sl of list) {\n allSerializedLayers.push(sl);\n if (sl.children && Array.isArray(sl.children)) {\n collectSerialized(sl.children as Array<{ id: string; asset?: string; source?: string; children?: unknown[] }>);\n }\n }\n };\n collectSerialized(serialized.layers ?? []);\n\n const sLayerMap = new Map(allSerializedLayers.map((sl) => [sl.id, sl]));\n const imgW = state.imageDimensions?.width || 1;\n const imgH = state.imageDimensions?.height || 1;\n\n return ctrlLayers.map((l) => {\n const bounds = controller.getLayerBounds(l.id) || {\n x: 0,\n y: 0,\n width: imgW,\n height: imgH,\n };\n const sLayer = sLayerMap.get(l.id);\n const assetKey = sLayer?.asset;\n const layerDataUrl =\n (assetKey && assets[assetKey]) ||\n assets[l.id] ||\n (sLayer?.source === 'document' ? fallbackImageUrl : '');\n return {\n id: l.id,\n name: l.name || 'Untitled Layer',\n imageUrl: layerDataUrl || fallbackImageUrl,\n thumbnailUrl: layerDataUrl || undefined,\n visible: l.visible !== false,\n opacity: l.opacity ?? 1,\n x: imgW ? bounds.x / imgW : 0,\n y: imgH ? bounds.y / imgH : 0,\n width: imgW ? bounds.width / imgW : 1,\n height: imgH ? bounds.height / imgH : 1,\n };\n });\n}\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/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/color-management.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/color-tools.ts","../../src/image-editor/retouch-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/transform-tools.ts","../../src/image-editor/smart-tools.ts","../../src/image-editor/shape-tools.ts","../../src/image-editor/type-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/shape-type-ui.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/effects-ui.ts","../../src/image-editor/layers/color-ui.ts","../../src/image-editor/psd/ui.ts","../../src/image-editor/layers/transform-ui.ts"],"names":["px","b","paint","data","out","pct","WRITE_IDEMPOTENT","LAYER_ID","opt","proof","BRUSH","TARGET","summary","id","DEFS","GEOMETRY","pick","spec","mod","bestD","q","partner"],"mappings":"AAAA;AACE;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;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;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;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;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;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACtoBO,IAAM,eAAA,EAAiB,GAAA;AAgCvB,SAAS,gBAAA,CAAiB,SAAA,EAAwC;AACvE,EAAA,MAAM,IAAA,EAAM,IAAI,WAAA,CAAY,cAAc,CAAA;AAC1C,EAAA,MAAM,MAAA,EAAQ,IAAI,WAAA,CAAY,cAAc,CAAA;AAC5C,EAAA,MAAM,KAAA,EAAO,IAAI,WAAA,CAAY,cAAc,CAAA;AAC3C,EAAA,MAA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AAA,EAAI,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA;AACA,EAAA;AACE,IAAA;AACH,IAAA;AACnB,EAAA;AACgB,EAAA;AAClB;AAE8B;AD0lBP;AACA;AE3rBP;AACI,EAAA;AACM,EAAA;AACN,EAAA;AACH,EAAA;AACY,EAAA;AAET,EAAA;AACJ,IAAA;AACM,IAAA;AACC,MAAA;AACL,MAAA;AACF,MAAA;AACE,QAAA;AACF,QAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AAEkB,EAAA;AACJ,IAAA;AACM,IAAA;AACC,MAAA;AACL,MAAA;AACF,MAAA;AACK,QAAA;AACL,QAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AACoB,EAAA;AACtB;AAagB;AACI,EAAA;AACC,EAAA;AACH,EAAA;AACI,EAAA;AACT,IAAA;AACI,IAAA;AACA,IAAA;AACK,IAAA;AACpB,EAAA;AACO,EAAA;AACT;AF+qBuB;AACA;AG9tBO;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;AACD,IAAA;AACf,EAAA;AACW,EAAA;AACP,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,EAAA;AACF;AAES;AACoB,EAAA;AACX,EAAA;AACC,IAAA;AACJ,IAAA;AACb,EAAA;AACO,EAAA;AACT;AJylBuB;AACA;AK3/DP;AAMA,EAAA;AACM,EAAA;AACR,EAAA;AACW,EAAA;AACH,EAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACH,IAAA;AACK,IAAA;AACtB,EAAA;AACqB,EAAA;AACV,EAAA;AACW,EAAA;AACA,EAAA;AACL,EAAA;AACE,IAAA;AACD,IAAA;AACE,MAAA;AACF,MAAA;AACH,MAAA;AACC,MAAA;AACA,MAAA;AACI,MAAA;AACE,QAAA;AAClB,MAAA;AACY,MAAA;AACI,MAAA;AACH,QAAA;AACA,QAAA;AACG,QAAA;AACH,QAAA;AACA,QAAA;AACG,QAAA;AACR,UAAA;AACA,UAAA;AACC,UAAA;AACO,YAAA;AACC,YAAA;AACb,UAAA;AACa,UAAA;AACf,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACoB,EAAA;AACb,EAAA;AACT;AAE+B;AACb,EAAA;AACI,EAAA;AACb,EAAA;AACT;AAGgB;AAMO,EAAA;AACvB;AAG8B;AACb,EAAA;AACA,EAAA;AACA,EAAA;AACjB;AAG8B;AAClB,EAAA;AACE,EAAA;AACO,IAAA;AACJ,IAAA;AACA,IAAA;AACM,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAG8B;AAClB,EAAA;AACW,EAAA;AACR,EAAA;AACI,EAAA;AACA,EAAA;AACC,EAAA;AAEA,EAAA;AACC,EAAA;AACQ,EAAA;AACP,EAAA;AACE,IAAA;AACD,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAGgB;AACP,EAAA;AACT;ALu+DuB;AACA;AM3lEF;AACC,EAAA;AACR,EAAA;AACM,IAAA;AAClB,EAAA;AACoB,EAAA;AACb,EAAA;AACT;AAEwB;AACR,EAAA;AACM,EAAA;AACJ,IAAA;AACF,IAAA;AACd,EAAA;AACO,EAAA;AACN;AAE8B;AAGK;AACzB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACM,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACrB;AAGoB;AACD,EAAA;AACI,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACJ,EAAA;AACI,IAAA;AACF,IAAA;AACA,IAAA;AACH,IAAA;AACE,MAAA;AACG,MAAA;AAGD,MAAA;AACC,MAAA;AACD,MAAA;AACJ,MAAA;AACd,IAAA;AACF,EAAA;AACc,EAAA;AACE,IAAA;AACG,IAAA;AACD,IAAA;AAEF,IAAA;AAKhB,EAAA;AACF;AAOS;AACW,EAAA;AACG,EAAA;AACF,IAAA;AACD,MAAA;AACG,MAAA;AACP,MAAA;AACC,MAAA;AACG,MAAA;AACG,QAAA;AACD,QAAA;AACD,UAAA;AACD,UAAA;AACG,UAAA;AACN,UAAA;AACT,QAAA;AACF,MAAA;AACU,MAAA;AACI,QAAA;AACE,QAAA;AAChB,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAO0B;AACd,EAAA;AACQ,EAAA;AACF,EAAA;AACA,EAAA;AACC,EAAA;AACD,EAAA;AACE,EAAA;AACR,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AAEe,IAAA;AACZ,IAAA;AACC,MAAA;AACI,MAAA;AACC,MAAA;AACD,MAAA;AACF,MAAA;AACN,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACH,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACD,QAAA;AACD,QAAA;AACD,QAAA;AACE,QAAA;AACA,UAAA;AACD,UAAA;AACH,UAAA;AACV,QAAA;AACF,MAAA;AACF,IAAA;AACK,EAAA;AACW,IAAA;AACE,MAAA;AACA,MAAA;AACF,QAAA;AACG,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACf,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACoB,EAAA;AACN,EAAA;AACP,EAAA;AACL,IAAA;AACA,IAAA;AACe,IAAA;AACjB,EAAA;AACF;AAkBqB;AACF,EAAA;AACL,IAAA;AACQ,IAAA;AACC,IAAA;AACC,IAAA;AACtB,EA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,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;AACA,IAAA;AACE,MAAA;AACT,MAAA;AACS,MAAA;AACA,MAAA;AACjB,IAAA;AACF,EAAA;AACF;AAGuB;AACP,EAAA;AACe,IAAA;AACX,IAAA;AACG,MAAA;AACL,QAAA;AACI,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A;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACA,MAAA;AACG,MAAA;AACX,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACM,IAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACF,EAAA;AACF;AASa;AACU,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACC,EAAA;AACvB;AAGa;AACX,EAAA;AACM,IAAA;AACG,IAAA;AACE,IAAA;AACJ,IA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AACG,MAAA;AACD,MAAA;AACjB,IAAA;AACF,EAAA;AAEQ,EAAA;AACV;AhB42QuB;AACA;AiBtpRR;AACC;AACVC;AACgB;AAEI;AAGE;AACR,EAAA;AACmB,EAAA;AACxB,IAAA;AACG,IAAA;AACD,IAAA;AACC,IAAA;AACA,IAAA;AAChB,EAAA;AACkB,EAAA;AACG,IAAA;AACf,IAAA;AACN,EAAA;AACO,EAAA;AACT;AAQE;AAIqC,EAAA;AACzB,IAAA;AACE,IAAA;AACA,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACI,IAAA;AACJ,IAAA;AACK,IAAA;AACH,IAAA;AAGD,IAAA;AAChB,EAAA;AACoB,EAAA;AACb,IAAA;AACU,MAAA;AACE,MAAA;AACX,MAAA;AACW,MAAA;AACL,MAAA;AACV,MAAA;AACG,IAAA;AACc,MAAA;AACJ,MAAA;AACb,MAAA;AACG,IAAA;AACc,MAAA;AACjB,MAAA;AACG,IAAA;AACa,MAAA;AACD,MAAA;AACf,MAAA;AACG,IAAA;AACU,MAAA;AACT,MAAA;AACQ,MAAA;AACE,QAAA;AACA,QAAA;AACE,QAAA;AAChB,MAAA;AACU,MAAA;AACV,MAAA;AACG,IAAA;AACU,MAAA;AACT,MAAA;AACO,MAAA;AACG,QAAA;AACC,QAAA;AACD,QAAA;AACD,QAAA;AACG,QAAA;AAChB,MAAA;AACU,MAAA;AACV,MAAA;AACG,IAAA;AACU,MAAA;AACT,MAAA;AACa,MAAA;AACb,MAAA;AACa,MAAA;AACjB,MAAA;AACJ,EAAA;AACO,EAAA;AACT;AAEmB;AACD;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACH,EAAA;AACjB;AACsB;AAEC;AACM,EAAA;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACT,EAAA;AACT;AAUgB;AACI,EAAA;AACN,IAAA;AACE,IAAA;AACK,IAAA;AACV,IAAA;AACY,MAAA;AACH,MAAA;AACH,MAAA;AACb,IAAA;AACF,EAAA;AAC6B,EAAA;AACT,IAAA;AACL,IAAA;AACN,IAAA;AACT,EAAA;AAE8B,EAAA;AACtB,I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AA;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,MAAA;AACE,MAAA;AACF,MAAA;AACP,MAAA;AACA,QAAA;AACO,QAAA;AACC,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AACW,IAAA;AACX,IAAA;AACF,EAAA;AACO,EAAA;AACC,IAAA;AACO,IAAA;AACM,IAAA;AACA,IAAA;AACnB,IAAA;AACF,EAAA;AACM,EAAA;AACS,IAAA;AACN,IAAA;AACL,MAAA;AACQ,QAAA;AACQ,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACQ,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACM,QAAA;AACF,UAAA;AACK,UAAA;AACL,UAAA;AACC,UAAA;AACA,UAAA;AACD,UAAA;AACC,UAAA;AACX,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAEkB;AAGT;AACM,EAAA;AACN,EAAA;AACC,IAAA;AACO,IAAA;AAGD,IAAA;AACO,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACA,MAAA;AACC,MAAA;AACJ,MAAA;AACI,MAAA;AACD,MAAA;AACD,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACC,MAAA;AACD,MAAA;AACE,MAAA;AACC,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AACF,MAAA;AACG,MAAA;AACJ,MAAA;AACEE,MAAAA;AACE,MAAA;AACA,MAAA;AAChB,MAAA;AACe,MAAA;AACA,MAAA;AACE,MA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ACH,IAAA;AACO,IAAA;AACD,IAAA;AACC,MAAA;AACC,MAAA;AACC,MAAA;AACf,IAAA;AACW,IAAA;AACX,IAAA;AACF,EAAA;AACO,EAAA;AAEH,IAAA;AACK,IAAA;AACL,MAAA;AACQ,QAAA;AACO,QAAA;AACD,QAAA;AACF,UAAA;AACL,UAAA;AACO,UAAA;AACD,UAAA;AACX,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACQ,MAAA;AACR,MAAA;AACQ,QAAA;AACO,QAAA;AACC,QAAA;AACH,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACO,QAAA;AACD,QAAA;AACF,UAAA;AACL,UAAA;AACO,UAAA;AACE,UAAA;AACd,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACA,MAAA;AACQ,QAAA;AACO,QAAA;AACD,QAAA;AACF,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACE,UAAA;AACE,UAAA;AACA,UAAA;AACC,UAAA;AACf,QAAA;AACW,QAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACU,EAAA;AACK,IAAA;AACN,IAAA;AACG,MAAA;AACR,MAAA;AACQ,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACD,YAAA;AACD,cAAA;AACN,cAAA;AACA,cAAA;AACO,cAAA;AACC,gBAAA;AACN,gBAAA;AACA,gBAAA;AACK,kBAAA;AACA,kBAAA;AACC,kBAAA;AACC,kBAAA;AACL,kBAAA;AACF,gBAAA;AACA,gBAAA;AACA,gBAAA;AACF,cAAA;AACF,YAAA;AACU,YAAA;AACZ,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACU,EAAA;AACF,IAAA;AACO,IAAA;AACD,IAAA;AACH,MAAA;AACC,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACD,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACc,MAAA;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CI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACX,EAAA;AACF;AAGgB;AACD,EAAA;AACf;AAG6B;AACR,EAAA;AACC,IAAA;AACpB,EAAA;AACO,EAAA;AACT;AAMyB;AACR,EAAA;AACG,EAAA;AACpB;AAG8B;AACb,EAAA;AACA,EAAA;AACK,EAAA;AACP,EAAA;AAEG,EAAA;AAClB;AAkC0B;AACH,EAAA;AACN,EAAA;AACM,EAAA;AACJ,EAAA;AACG,EAAA;AACL,EAAA;AACC,EAAA;AACT,EAAA;AACK,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;AACZ,EAAA;AACF;AAGsB;AACL,EAAA;AACI,EAAA;AACf,EAAA;AACc,IAAA;AACT,MAAA;AACQ,QAAA;AACX,QAAA;AACG,MAAA;AACQ,QAAA;AACX,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;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACG,QAAA;AACN,QAAA;AACG,MAAA;AACG,QAAA;AACN,QAAA;AACG,MAAA;AACA,MAAA;AACQ,QAAA;AACX,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACO,QAAA;AACV,QAAA;AACG,MAAA;AACO,QAAA;AACV,QAAA;AACG,MAAA;AACW,QAAA;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;AAmBgB;AAIA,EAAA;AACK,EAAA;AACA,EAAA;AAEA,EAAA;AAIJ,EAAA;AACT,EAAA;AAEA,EAAA;AACa,IAAA;AACf,MAAA;AACO,MAAA;AACL,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACkB,EAAA;AAEA,EAAA;AACC,EAAA;AACA,EAAA;AAED,EAAA;AACD,IAAA;AACV,MAAA;AACA,MAAA;AACI,MAAA;AACC,MAAA;AACV,IAAA;AACe,IAAA;AACE,IAAA;AACX,IAAA;AAIC,IAAA;AACC,MAAA;AACU,MAAA;AACN,MAAA;AACI,MAAA;AACH,MAAA;AACA,MAAA;AACM,MAAA;AACA,MAAA;AACH,MAAA;AACC,MAAA;AACjB,IAAA;AACD,EAAA;AACH;AAIa;AACQ,EAAA;AACC,EAAA;AACA,EAAA;AACtB;AjCu7YuB;AACA;AkCrvZP;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,MAAA;AACF,QAAA;AACA,QAAA;AACI,QAAA;AACQ,QAAA;AACJ,QAAA;AACA,QAAA;AACE,QAAA;AACA,QAAA;AACf,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACA,QAAA;AACM,QAAA;AACN,QAAA;AACI,QAAA;AACE,QAAA;AACN,QAAA;AACM,QAAA;AACT,QAAA;AACG,QAAA;AACA,QAAA;AACH,QAAA;AACG,QAAA;AACA,QAAA;AACH,QAAA;AACN,MAAA;AACG,IAAA;AACI,MAAA;AACF,QAAA;AACI,QAAA;AACM,QAAA;AACE,QAAA;AACJ,QAAA;AACJ,QAAA;AACT,MAAA;AACG,IAAA;AACQ,MAAA;AACR,IAAA;AACQ,MAAA;AACR,IAAA;AACI,MAAA;AACF,QAAA;AACI,QAAA;AACM,QAAA;AACA,QAAA;AACE,QAAA;AACjB,MAAA;AACG,IAAA;AACI,MAAA;AACF,QAAA;AACQ,QAAA;AACJ,QAAA;AACJ,QAAA;AACL,MAAA;AACJ,EAAA;AACF;AAGoB;AACL,EAAA;AACA,EAAA;AACE,EAAA;AACjB;AAOgB;AACK,EAAA;AACP,EAAA;AACA,EAAA;AACG,IAAA;AACG,IAAA;AAClB,EAAA;AACkB,EAAA;AAEN,EAAA;AACG,IAAA;AACI,IAAA;AACnB,EAAA;AACgB,EAAA;AAClB;AlCitZuB;AACA;AmC/7ZV;AAGA;AAE0E;AACnE,EAAA;AACE,EAAA;AACA,EAAA;AACF,EAAA;AACpB;AAGa;AACS,EAAA;AACC,EAAA;AACD,EAAA;AACtB;AAGgB;AACE,EAAA;AACF,EAAA;AACP,EAAA;AACQ,IAAA;AACC,IAAA;AACA,IAAA;AACG,IAAA;AACA,IAAA;AACnB,EAAA;AACF;AnC07ZuB;AACA;AoC99ZQ;AAEZ;AACD,EAAA;AACI,EAAA;AACC,IAAA;AACH,IAAA;AACC,IAAA;AACV,IAAA;AACR,EAAA;AACH;AAGsB;AACF,EAAA;AACpB;AAG4B;AACR,EAAA;AACA,EAAA;AACpB;AAMsB;AACF,EAAA;AACD,EAAA;AACN,EAAA;AACQ,IAAA;AACD,IAAA;AACG,IAAA;AACV,IAAA;AACI,IAAA;AACL,IAAA;AACS,IAAA;AACnB,EAAA;AACmB,EAAA;AACrB;ApCs9ZuB;AACA;AqC78ZA;AACE;AACsB;AACU;AAEC;AACS;AAChD,EAAA;AACL,EAAA;AACM,EAAA;AACL,EAAA;AACI,EAAA;AACH,EAAA;AACM,EAAA;AACL,EAAA;AACM,EAAA;AACvB;AAWa;AACI,EAAA;AACC,EAAA;AACK,EAAA;AACJ,EAAA;AACI,EAAA;AACA,EAAA;AACvB;AAGa;AACK,EAAA;AACI,EAAA;AACtB;AAGa;AACS,EAAA;AACA,EAAA;AACA,EAAA;AACtB;AAca;AACO,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACjB;AAoBgD;AAC1C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACQ,EAAA;AACH,EAAA;AACC,EAAA;AACX;AACkD;AAEX;AACI;AACA;AAEtB;AACE,EAAA;AACA,EAAA;AACH,EAAA;AACE,EAAA;AACtB;AAEwC;AAC3B,EAAA;AACS,EAAA;AACL,IAAA;AACI,IAAA;AACE,IAAA;AACD,IAAA;AACH,IAAA;AACG,IAAA;AACX,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAGuB;AACR,EAAA;AACO,EAAA;AACF,IAAA;AACA,IAAA;AACE,IAAA;AACpB,EAAA;AACO,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ACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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 * 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, type ShapeLayer, type TypeLayer } 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): boolean =>\n l.type === 'annotation'\n ? l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask\n : (l.type === 'shape' || l.type === 'type') && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation!.blend ?? 'normal') && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;\n\nconst overlayViews = new WeakMap<ImageLayer, AnnotationLayer>();\n\n/** An annotation layer, or the annotation view of a shape layer migrated from one (memoised). */\nfunction overlayView(l: ImageLayer): AnnotationLayer {\n if (l.type === 'annotation') return l;\n let view = overlayViews.get(l);\n if (!view) {\n const shape = l as ShapeLayer | TypeLayer;\n view = { type: 'annotation', id: shape.id, name: shape.name, visible: shape.visible, opacity: 1, blend: 'normal', annotation: shape.annotation! };\n overlayViews.set(l, view);\n }\n return view;\n}\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 and shape and type layers that still are the annotation they were\n * migrated from (given as annotation views; 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.type === 'shape' || l.type === 'type') && l.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).filter((l) => l.visible).map(overlayView),\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 type { PixelTransform } from './color-management';\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 /**\n * On-canvas text editing (`TextEditSession.overlay()`): the caret as a segment, the selected\n * ranges as quads and the text's frame, in natural pixels.\n */\n textEdit?: { caret: [ImagePoint, ImagePoint] | null; selection: ImagePoint[][]; frame: ImagePoint[] | null } | 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 /** Colour transform from the document to the display (see `color-management`), or none. */\n private display: PixelTransform | null = null;\n private displayKey = 0;\n\n /** Sets the viewport's display transform (document profile → sRGB, or a soft proof); `null` for none. */\n public setDisplayTransform(t: PixelTransform | null): void {\n if (t === this.display) return;\n this.display = t;\n this.displayKey++;\n // Every tile is uploaded again through the new transform.\n this.comp = null;\n this.cachedBaseKey = '';\n }\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 `d${this.displayKey}`,\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 this.display?.(pixels.data);\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 // Text editing: the frame (dashed), the selection (tinted) and the caret, a fixed width on screen.\n if (options.textEdit) {\n const t = options.textEdit;\n ctx.save();\n try {\n const scr = (p: ImagePoint) => projectImagePoint(p, projection);\n const poly = (pts: ImagePoint[]) => {\n ctx.beginPath();\n pts.map(scr).forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));\n ctx.closePath();\n };\n if (t.frame) {\n poly(t.frame);\n ctx.setLineDash([4, 3]);\n ctx.lineWidth = 1;\n ctx.strokeStyle = 'rgba(99,102,241,0.9)';\n ctx.stroke();\n ctx.setLineDash([]);\n }\n ctx.fillStyle = 'rgba(99,102,241,0.35)';\n for (const q of t.selection) {\n poly(q);\n ctx.fill();\n }\n if (t.caret) {\n const [a, b] = t.caret.map(scr);\n ctx.beginPath();\n ctx.moveTo(a.x, a.y);\n ctx.lineTo(b.x, b.y);\n ctx.lineWidth = 3;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n ctx.lineWidth = 1.5;\n ctx.strokeStyle = '#111827';\n ctx.stroke();\n }\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 this.display?.(img.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 = `${this.displayKey}:${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 { applyHomography, homographyFromAffine, multiplyHomographies, type Homography } from '../geom/homography';\nimport { invertMatrix as invertAffine } from '../geom/affine';\nimport { findLayer, insertLayer, mapLayer, newLayerId, type ImageLayer, type LayerTarget, type PixelLayer, type RasterMask, type ShapeLayer, type SmartObjectLayer } from './layers/model';\nimport { renderShape, shapeGeometryBounds, transformShapeComponents, type ShapeEnv } from './layers/shape';\nimport { rerenderText, textMatrix, typeFrame, type TextEnv } from './layers/text';\nimport type { TypeLayer } 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 matrixFromCorners,\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 /** What shape layers render with (canvas size, document patterns). */\n shapeEnv(): ShapeEnv;\n /** What type layers lay out and draw with. */\n textEnv(): TextEnv;\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 /** True when the layer is a shape layer (the transform maps its paths and renders them again). */\n readonly shapeLayer: boolean = false;\n private readonly shape: ShapeLayer | null = null;\n /** True when the layer is a type layer (the transform sets its text matrix; affine only). */\n readonly typeLayer: boolean = false;\n private readonly typeTarget: TypeLayer | null = null;\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' || target?.type === 'shape' || target?.type === 'type') && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : 'pixels')) !== 'mask';\n // A smart object or a shape 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' && layer.type !== 'shape' && layer.type !== 'type') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, 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 (layer.type === 'type' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n // As a smart object: the frame is the text's own box, the corners where it lands now.\n const frame = typeFrame(layer, host.textEnv().shaper);\n const m = textMatrix(layer);\n const at = (x: number, y: number) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });\n this.typeTarget = layer;\n this.typeLayer = true;\n this.box = frame;\n this.startGeometry = {\n box: frame,\n corners: [at(frame.x, frame.y), at(frame.x + frame.width, frame.y), at(frame.x + frame.width, frame.y + frame.height), at(frame.x, frame.y + frame.height)],\n warp: null,\n };\n this.geometryValue = this.startGeometry;\n this.startInterp = this.interp;\n this.moves = mask && linked ? ['pixels', 'mask'] : ['pixels'];\n if (mask && linked) this.parts.push({ what: 'mask', offset: o, moving: mask.surface, base: null, reduced: new Map() });\n this.shown = this.startLayers;\n return;\n }\n if (layer.type === 'shape' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n const g = shapeGeometryBounds(layer);\n if (!g) fail(`Shape layer \"${layer.name || id}\" has no path to transform.`);\n // A straight line has no height (or width): give the frame one pixel so it can turn.\n const w = Math.max(g!.width, 1);\n const h = Math.max(g!.height, 1);\n box = { x: g!.x + (g!.width - w) / 2, y: g!.y + (g!.height - h) / 2, width: w, height: h };\n this.shape = layer;\n this.shapeLayer = true;\n moves.push('pixels');\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 }\n } else 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 if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {\n fail('Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.');\n }\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 if (this.typeTarget) return { layers: this.resampleType(), 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 if (this.shape) layers = this.resampleShape(final, preview, warp ? f : null, h);\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 /**\n * A shape with its paths mapped through the current geometry and rendered again (live shapes\n * stay live under an affine map; perspective and warps turn them into plain paths). Previews\n * render only the visible part.\n */\n private resampleShape(final: boolean, preview: FreeTransformPreview, warpMap: ((p: { x: number; y: number }) => { x: number; y: number } | null) | null, h: Homography | null): ImageLayer[] {\n const layer = this.shape!;\n let components = layer.components;\n if (!isIdentityTransform(this.geometryValue)) {\n const affine = !warpMap && h && Math.abs(h[6]) < 1e-12 && Math.abs(h[7]) < 1e-12 && Math.abs(h[8] - 1) < 1e-12 ? ([h[0], h[3], h[1], h[4], h[2], h[5]] as const) : undefined;\n const map = warpMap ?? ((p: { x: number; y: number }) => (h ? applyHomography(h, p) : p));\n components = transformShapeComponents(layer.components, (p) => map(p) ?? p, affine);\n }\n const moved: ShapeLayer = { ...layer, components };\n delete moved.annotation;\n const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });\n return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));\n }\n\n /**\n * A type layer with its text matrix set from the corners (affine only: scale, rotate, skew and\n * flips), drawn again; a linked mask follows by the change's homography.\n */\n private resampleType(): ImageLayer[] {\n const layer = this.typeTarget!;\n const g = this.geometryValue;\n if (hasWarp(g)) fail('Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.');\n const m = matrixFromCorners(g.box, g.corners);\n if (!m) fail('Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.');\n const next: TypeLayer = { ...layer, transform: m! };\n delete next.annotation;\n let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));\n // A linked mask follows the change of the matrix.\n const change = multiplyHomographies(homographyFromAffine(invertAffine(textMatrix(layer))), homographyFromAffine(m!));\n for (const part of this.parts) {\n if (part.what !== 'mask') continue;\n const surface = transformSurface(part.moving, change, { interpolation: this.interp === 'nearest' ? 'nearest' : 'bilinear' });\n layers = mapLayer(layers, layer.id, (l) => ({ ...l, mask: { ...(l.mask as RasterMask), surface } }) as ImageLayer);\n }\n return layers;\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 && !this.typeTarget) 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' || layer?.type === 'shape' || layer?.type === 'type')) 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","/**\n * Colour management for the image editor, on `@pixerate/editor/cms`:\n *\n * - The document has a colour profile (its working space): the colours its RGB pixels mean.\n * Missing means sRGB. Assign Profile changes that meaning and keeps the numbers; Convert to\n * Profile changes the numbers so the colours stay (with a rendering intent and black point\n * compensation).\n * - The viewport assumes an sRGB display: a document in another space is converted for display,\n * and Proof Setup shows it as it would print or display through another profile (soft\n * proofing), optionally with a gamut warning.\n * - Profiles travel with files: PSD image resource 1039, PNG `iCCP`, JPEG APP2.\n */\n\nimport {\n BUILTIN_PROFILES,\n CmsError,\n GamutCheck,\n Transform,\n builtinProfile,\n builtinProfileId,\n encodeProfile,\n parseProfile,\n sameColors,\n srgbProfile,\n type BuiltinProfileId,\n type Intent,\n type Profile,\n} from '../cms';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { TiledSurface, finishTile, type Tile } from '../raster/surface';\nimport { deepTile, srgbDecode, srgbEncode } from '../raster/deep';\n\n/** A document's colour profile: a built-in one by id, or embedded ICC bytes. */\nexport interface DocumentColorProfile {\n /** A built-in profile (see `BUILTIN_PROFILES`). */\n builtin?: BuiltinProfileId;\n /** The profile's description. */\n name: string;\n /** ICC bytes of a profile that is not built in. */\n icc?: Uint8Array;\n}\n\n/** View › Proof Setup (view state). */\nexport interface ProofSetup {\n profile: DocumentColorProfile;\n /** Rendering intent from the document to the proof profile (default relative). */\n intent: Intent;\n /** Black point compensation (default true). */\n bpc: boolean;\n /** Show the proof's paper white and ink black (absolute colorimetric to the display). */\n simulatePaper: boolean;\n /** Grey out colours the proof profile cannot reproduce (Gamut Warning). */\n gamutWarning: boolean;\n}\n\nexport const DEFAULT_PROOF: Omit<ProofSetup, 'profile'> = { intent: 'relative', bpc: true, simulatePaper: false, gamutWarning: false };\n\n/**\n * Image › Mode beyond RGB. The document is still edited as RGB in its working space; the mode is\n * what it is for: the viewport soft-proofs through the CMYK profile (unless Proof Setup says\n * otherwise) and PSD export writes CMYK or Lab channels.\n */\nexport type DocumentColorMode = { mode: 'cmyk'; profile: DocumentColorProfile; intent: Intent } | { mode: 'lab' };\n\nexport interface SerializedColorMode {\n mode: 'cmyk' | 'lab';\n profile?: SerializedColorProfile;\n intent?: Intent;\n}\n\nexport function serializeColorMode(m: DocumentColorMode): SerializedColorMode {\n return m.mode === 'cmyk' ? { mode: 'cmyk', profile: serializeColorProfile(m.profile), intent: m.intent } : { mode: 'lab' };\n}\n\nexport function deserializeColorMode(raw: unknown, problems: string[]): DocumentColorMode | null {\n const r = raw as SerializedColorMode;\n if (!r || typeof r !== 'object') return null;\n if (r.mode === 'lab') return { mode: 'lab' };\n if (r.mode !== 'cmyk') return null;\n const profile = (r.profile && deserializeColorProfile(r.profile, problems)) || documentProfile('coated-cmyk');\n const intent = (['perceptual', 'relative', 'saturation', 'absolute'] as const).includes(r.intent as Intent) ? (r.intent as Intent) : 'relative';\n return { mode: 'cmyk', profile, intent };\n}\n\n/** The proof the viewport shows: Proof Setup, else a CMYK document's own profile. */\nexport function effectiveProof(proof: ProofSetup | null | undefined, mode: DocumentColorMode | null | undefined): ProofSetup | null {\n if (proof) return proof;\n if (mode?.mode === 'cmyk') return { ...DEFAULT_PROOF, profile: mode.profile, intent: mode.intent };\n return null;\n}\n\n/**\n * RGBA pixels (in the document profile `doc`) as the channel planes of `mode`, for a PSD:\n * CMYK as Photoshop stores it (255 = no ink), Lab with L 0..255 for 0..100 and a, b offset by 128.\n */\nexport function colorModePlanes(doc: DocumentColorProfile | null | undefined, mode: DocumentColorMode): (rgba: Uint8ClampedArray, n: number) => Uint8Array[] {\n const src = resolveProfile(doc);\n if (mode.mode === 'cmyk') {\n const t = Transform.create(src, resolveProfile(mode.profile), { intent: mode.intent, bpc: true });\n return (rgba, n) => {\n const ink = new Uint8Array(n * 4);\n t.convert8(rgba, 4, ink, 4, false);\n const planes = [0, 1, 2, 3].map(() => new Uint8Array(n));\n for (let i = 0; i < n; i++) for (let c = 0; c < 4; c++) planes[c][i] = 255 - ink[i * 4 + c];\n return planes;\n };\n }\n const t = Transform.create(src, builtinProfile('lab-d50'), { intent: 'relative', bpc: false });\n return (rgba, n) => {\n const lab = new Uint8Array(n * 4);\n t.convert8(rgba, 4, lab, 4, false);\n const planes = [0, 1, 2].map(() => new Uint8Array(n));\n for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = lab[i * 4 + c];\n return planes;\n };\n}\n\nconst resolved = new WeakMap<object, Profile>();\nconst fromBytes = new Map<string, Profile>();\n\n/** The profile a document profile names (sRGB when missing). Throws `CmsError` for unreadable ICC bytes. */\nexport function resolveProfile(doc: DocumentColorProfile | null | undefined): Profile {\n if (!doc) return srgbProfile();\n const hit = resolved.get(doc);\n if (hit) return hit;\n let p: Profile;\n if (doc.builtin) p = builtinProfile(doc.builtin);\n else if (doc.icc) {\n const key = bytesToBase64(doc.icc);\n p = fromBytes.get(key) ?? parseProfile(doc.icc);\n if (fromBytes.size > 32) fromBytes.clear();\n fromBytes.set(key, p);\n } else p = srgbProfile();\n resolved.set(doc, p);\n return p;\n}\n\n/** The ICC bytes to embed for a document profile. */\nexport function profileBytes(doc: DocumentColorProfile | null | undefined): Uint8Array {\n if (doc?.icc) return doc.icc;\n const p = resolveProfile(doc);\n return p.bytes ?? encodeProfile(p);\n}\n\n/**\n * A document profile from what a user or agent gives: a built-in id or alias (\"srgb\",\n * \"display-p3\", \"Adobe RGB (1998)\", \"coated-cmyk\", …) or ICC bytes. Throws with the choices.\n */\nexport function documentProfile(input: string | Uint8Array): DocumentColorProfile {\n if (typeof input === 'string') {\n const id = builtinProfileId(input);\n if (!id) throw new CmsError(`Unknown colour profile \"${input}\"; use one of ${BUILTIN_PROFILES.map((b) => b.id).join(', ')}, or ICC bytes.`);\n return { builtin: id, name: BUILTIN_PROFILES.find((b) => b.id === id)!.description };\n }\n const p = parseProfile(input);\n // A copy of a built-in profile (Photoshop's own sRGB, say) is stored as the built-in one.\n return { name: p.description || 'Embedded profile', icc: input.slice() };\n}\n\n/** Whether a document profile means sRGB (so the display needs no conversion). */\nexport function isSrgb(doc: DocumentColorProfile | null | undefined): boolean {\n if (!doc || doc.builtin === 'srgb') return true;\n try {\n return sameColors(resolveProfile(doc), srgbProfile(), builtinProfile('lab-d50'));\n } catch {\n return false;\n }\n}\n\n/** JSON form of a document profile. */\nexport interface SerializedColorProfile {\n builtin?: BuiltinProfileId;\n name: string;\n /** ICC bytes, base64. */\n icc?: string;\n}\n\nexport function serializeColorProfile(doc: DocumentColorProfile): SerializedColorProfile {\n return doc.builtin ? { builtin: doc.builtin, name: doc.name } : { name: doc.name, icc: bytesToBase64(doc.icc ?? new Uint8Array(0)) };\n}\n\n/** A document profile from JSON, or `null` (with a problem noted) when it cannot be read. */\nexport function deserializeColorProfile(raw: unknown, problems: string[]): DocumentColorProfile | null {\n const r = raw as SerializedColorProfile;\n if (!r || typeof r !== 'object') return null;\n try {\n if (r.builtin) {\n const id = builtinProfileId(r.builtin);\n if (!id) throw new CmsError(`unknown built-in profile \"${r.builtin}\"`);\n return documentProfile(id);\n }\n if (typeof r.icc === 'string') {\n const icc = base64ToBytes(r.icc);\n parseProfile(icc);\n return { name: typeof r.name === 'string' ? r.name : 'Embedded profile', icc };\n }\n } catch (e) {\n problems.push(`The colour profile could not be read (${e instanceof Error ? e.message : String(e)}); the document is treated as sRGB`);\n }\n return null;\n}\n\n/** Applies a colour transform to 8-bit RGBA in place (alpha kept). */\nexport type PixelTransform = (rgba: Uint8ClampedArray) => void;\n\n/**\n * What the viewport applies to the composite before showing it on an sRGB display: nothing for\n * an sRGB document without proofing; the document → sRGB conversion; or the proof chain\n * document → proof → sRGB, with out-of-gamut colours greyed when asked.\n */\nexport function displayTransform(doc: DocumentColorProfile | null | undefined, proof: ProofSetup | null | undefined): PixelTransform | null {\n const src = resolveProfile(doc);\n const display = srgbProfile();\n let t: Transform | null = null;\n let gamut: GamutCheck | null = null;\n if (proof) {\n const p = resolveProfile(proof.profile);\n t = Transform.proof(src, p, display, proof.intent, proof.bpc, proof.simulatePaper);\n if (proof.gamutWarning) gamut = new GamutCheck(src, p, builtinProfile('lab-d50'));\n } else if (!isSrgb(doc)) t = Transform.create(src, display, { intent: 'relative', bpc: true });\n if (!t) return null;\n const tr = t;\n const g = gamut;\n return (rgba) => {\n if (g) {\n const px = [0, 0, 0];\n const flags = new Uint8Array(rgba.length >> 2);\n for (let i = 0, p = 0; i < rgba.length; i += 4, p++) {\n if (rgba[i + 3] === 0) continue;\n px[0] = rgba[i] / 255;\n px[1] = rgba[i + 1] / 255;\n px[2] = rgba[i + 2] / 255;\n if (g.outOfGamut(px)) flags[p] = 1;\n }\n tr.convert8(rgba, 4, rgba, 4);\n for (let i = 0, p = 0; i < rgba.length; i += 4, p++) if (flags[p]) rgba[i] = rgba[i + 1] = rgba[i + 2] = 128;\n return;\n }\n tr.convert8(rgba, 4, rgba, 4);\n };\n}\n\n/** A `#rrggbb` colour converted between two profiles. */\nexport function convertHex(hex: string, from: Profile, to: Profile, intent: Intent = 'relative', bpc = true): string {\n const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());\n if (!m) return hex;\n const n = parseInt(m[1], 16);\n const out = new Float32Array(4);\n Transform.create(from, to, { intent, bpc }).eval([((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255], out);\n const c = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0');\n return `#${c(out[0])}${c(out[1])}${c(out[2])}`;\n}\n\n/**\n * A layer surface with every pixel's colour converted through `t` (an RGB → RGB transform):\n * 8-bit tiles through its tables, 16-bit tiles at 16 bits, 32-bit (linear) tiles through the\n * float path. Alpha is kept.\n */\nexport function convertSurfaceColors(surface: TiledSurface, t: Transform): TiledSurface {\n if (surface.channels !== 4 || t.inputs !== 3 || t.outputs !== 3) return surface;\n const tiles = new Map<number, Tile>();\n const memo = new Map<Tile, Tile | null>();\n for (const [key, tile] of surface.tiles) {\n let next = memo.get(tile);\n if (next === undefined) {\n if (tile.deep instanceof Uint16Array) {\n const out = new Uint16Array(tile.deep.length);\n t.convert16(tile.deep, 4, out, 4);\n next = deepTile(out, 4, surface.defaultValue);\n } else if (tile.deep instanceof Float32Array) {\n const enc = new Float32Array(tile.deep.length);\n for (let i = 0; i < enc.length; i += 4) {\n for (let c = 0; c < 3; c++) enc[i + c] = srgbEncode(tile.deep[i + c]);\n enc[i + 3] = tile.deep[i + 3];\n }\n t.convertFloat(enc, 4, enc, 4);\n for (let i = 0; i < enc.length; i += 4) for (let c = 0; c < 3; c++) enc[i + c] = srgbDecode(enc[i + c]);\n next = deepTile(enc, 4, surface.defaultValue);\n } else {\n const out = new Uint8ClampedArray(tile.data.length);\n t.convert8(tile.data, 4, out, 4);\n next = finishTile(out, 4, surface.defaultValue);\n }\n memo.set(tile, next);\n }\n if (next) tiles.set(key, next);\n }\n return new TiledSurface(4, tiles, surface.defaultValue);\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 { isBlendMode, 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 type LiveShape,\n type ShapeComponent,\n type ShapeFillRule,\n type ShapeOperation,\n type ShapeLayer,\n type ShapePaint,\n type ShapeStroke,\n type ParagraphStyle,\n type TextStyle,\n type TypeLayer,\n hasSurface,\n} from './layers/model';\nimport {\n annotationOfShape,\n createShapeLayer,\n liveComponent,\n mergeShapeComponents as mergeShapeComponentsOp,\n normalizeLiveShape,\n normalizeShapePaint,\n normalizeShapeStroke,\n normalizeSubpaths,\n offsetShape,\n requireOperation,\n requireShapeLayer,\n rerenderShape,\n shapeAsPixelLayer,\n shapeFromAnnotation,\n shapeGeometryBounds,\n transformShapeComponents,\n updateShapeLayer,\n type ShapeEnv,\n} from './layers/shape';\nimport { bezierToSubpaths, roundSubpaths } from '../vector/geometry';\nimport {\n DEFAULT_TEXT_STYLE,\n annotationOfType,\n caretAt,\n createTypeLayer,\n layoutText,\n normalizeText,\n patchParagraphStyle,\n patchTextStyle,\n replaceTextRange,\n requireTypeLayer,\n restyleRange,\n styleAt,\n textIndexAt,\n textOutlines,\n textMatrix,\n textRange,\n toTextSpace,\n transformTypeLayer,\n typeAsPixelLayer,\n typeLayerName,\n updateTypeLayer,\n type TextEnv,\n type TextLayout,\n type TextShaper,\n} from './layers/text';\nimport { createCanvasTextShaper } from './layers/text-shapers';\nimport type { VectorSubpath } from '../vector/types';\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 annotationOfLayer,\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 encodeSurface,\n serializePatterns,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayeredImageEditorState,\n} from './layers/serialize';\nimport { LayerCompositor, compositeDeep, 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 tileCoord,\n tileKey,\n} from '../raster/surface';\nimport { convertSurfaceDepth, isBitDepth, surfaceFromDeepPixels, to16From8, type BitDepth } from '../raster/deep';\nimport { workingToSamples } from '../raster/deep-compositor';\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 DEFAULT_RETOUCH,\n MixerStroke,\n RegionPixelSource,\n RetouchDabStroke,\n StrokeCoverage,\n TilePixelSource,\n applyRetouchToTile,\n applyStrokeToTile,\n cleanMixer,\n healPixels,\n loadMixer,\n normalizeBrush,\n normalizeMixer,\n normalizeRetouch,\n readSourceRegion,\n rgbaViewOfGrey,\n smudgeEffect,\n spongeEffect,\n spotHealPixels,\n spotHealWindow,\n toneOf,\n type BrushSettings,\n type MixerLoad,\n type MixerSettings,\n type MixerTarget,\n type PaintTileTarget,\n type PixelSource,\n type RetouchApply,\n type RetouchPaint,\n type RetouchSettings,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, encodePng, isPng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport type { Interpolation } from '../raster/transform';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd, prepareNestedLayers, smartContentsFile } from './psd/export';\nimport { BROWSER_IMAGE_TYPES } from './psd/smart';\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 { INTENTS, Transform, builtinProfile, canBeDestination, writeJpegIcc, writePngIcc, type BuiltinProfileId, type ColorSpace, type Intent, type Profile } from '../cms';\nimport {\n DEFAULT_PROOF,\n colorModePlanes,\n convertSurfaceColors,\n deserializeColorMode,\n effectiveProof,\n serializeColorMode,\n type DocumentColorMode,\n deserializeColorProfile,\n displayTransform,\n documentProfile,\n isSrgb,\n profileBytes,\n resolveProfile,\n serializeColorProfile,\n type DocumentColorProfile,\n type ProofSetup,\n} from './color-management';\nimport { applyMatrix, multiplyMatrices, rotateMatrix, 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/** 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 /** The retouching tools' options (aligned, sample, spot healing type, tone range, exposure, sponge mode, smudge strength). */\n retouch: RetouchSettings;\n /** The Clone Stamp and Healing Brush source point (Alt-click), or `null` when none is set. */\n cloneSource: CloneSource | null;\n}\n\n/** Where the Clone Stamp and Healing Brush sample from. */\nexport interface CloneSource {\n /** The source point, document pixels. */\n x: number;\n y: number;\n /** With Aligned on: the offset (source − stroke start) the first stroke fixed, kept for the next strokes; `null` until a stroke has used the point. */\n offset: { x: number; y: number } | null;\n}\n\n/**\n * The geometry of one shape component: a live shape (`{ kind: 'rect' | 'ellipse' | 'polygon' |\n * 'star' | 'line', … }`, see `LiveShape`) or subpaths (`[{ anchors: [{ x, y, in?, out? }],\n * closed }]` or SVG path data), and how it combines with the components below it.\n */\nexport interface ShapeGeometryInput {\n shape?: LiveShape | Record<string, unknown>;\n subpaths?: VectorSubpath[] | string;\n /** Default `combine`. */\n op?: ShapeOperation;\n}\n\n/** A stroke as tools and UIs give it: any `ShapeStroke` fields, the paint as a colour string or a paint (or `color`). */\nexport type ShapeStrokeInput = Omit<Partial<ShapeStroke>, 'paint'> & { paint?: ShapePaint | string; color?: string };\n\n/** A new shape layer: its geometry (one component, or `components`), fill and stroke. */\nexport interface ShapeLayerInit extends ShapeGeometryInput, LayerPlacement {\n /** Several components instead of `shape` / `subpaths`, bottom first. */\n components?: ShapeGeometryInput[];\n name?: string;\n /** A colour string or a paint; `null` for none. Default the foreground colour (none for a stroke-only open path). */\n fill?: ShapePaint | string | null;\n /** `{ width, paint, align?, cap?, join?, miterLimit?, dashes?, dashOffset? }`, or `null` / missing for none. */\n stroke?: ShapeStrokeInput | null;\n fillRule?: ShapeFillRule;\n opacity?: number;\n blend?: BlendMode;\n}\n\n/** A shape layer as tools and panels read it. */\nexport interface ShapeLayerInfo {\n id: string;\n name: string;\n /** Bottom to top; `index` is what component edits take. */\n components: Array<{ index: number; op: ShapeOperation; live: LiveShape | null; subpaths: VectorSubpath[]; bounds: CropBox | null }>;\n fillRule: ShapeFillRule;\n fill: ShapePaint | null;\n stroke: ShapeStroke | null;\n /** Bounds of the paths (document pixels, not rounded), or `null` when empty. */\n geometryBounds: CropBox | null;\n /** Bounds of the rendered pixels (stroke and antialiasing included). */\n bounds: CropBox | null;\n /** The annotation id while the layer is still the annotation it was migrated from. */\n annotationId?: string;\n}\n\n/** A new type layer: its text, where it goes, and its character and paragraph style. */\nexport interface TextLayerInit extends LayerPlacement {\n text: string;\n /**\n * Document x, y of the text's origin: point text's first baseline at its alignment edge (left\n * edge for left-aligned, centre for centred, right edge for right-aligned); a paragraph box's\n * top-left.\n */\n x: number;\n y: number;\n /** Paragraph (area) text: wrap inside a `width × height` box (pixels). Missing: point text. */\n box?: { width: number; height: number };\n /** Character style of the whole text (default 24 px sans-serif, the foreground colour). */\n style?: Partial<TextStyle>;\n paragraph?: ParagraphStyle;\n /** Degrees clockwise about the origin. */\n rotation?: number;\n name?: string;\n opacity?: number;\n blend?: BlendMode;\n}\n\n/** A type layer as tools and panels read it. Ranges are UTF-16 code units, `end` exclusive. */\nexport interface TypeLayerInfo {\n id: string;\n name: string;\n text: string;\n runs: Array<{ start: number; end: number; style: TextStyle }>;\n paragraphs: Array<{ start: number; end: number; style: ParagraphStyle }>;\n frame: TypeLayer['frame'];\n /** Text space → document. */\n transform: AffineMatrix;\n /** Laid-out lines: text range, baseline (text space) and width; empty without a shaper. */\n lines: Array<{ start: number; end: number; baseline: number; width: number }>;\n /** Rendered bounds in the document. */\n bounds: CropBox | null;\n /** Families drawn with a fallback because they are missing here. */\n missingFonts: string[];\n /** `live`: drawn here from the text; `stored`: pixels kept from a file until the next edit. */\n rendering: 'live' | 'stored';\n annotationId?: string;\n}\n\n/** The retouching tools (see `@pixerate/editor/paint`'s retouch module). */\nexport type RetouchTool = 'clone' | 'heal' | 'spot-heal' | 'smudge' | 'dodge' | 'burn' | 'sponge';\n\nexport const RETOUCH_TOOLS: readonly RetouchTool[] = ['clone', 'heal', 'spot-heal', 'smudge', 'dodge', 'burn', 'sponge'];\n\nexport const isRetouchTool = (tool: unknown): tool is RetouchTool => RETOUCH_TOOLS.includes(tool as RetouchTool);\n\n/** Paint tools a stroke can use. */\nexport type StrokeTool = 'brush' | 'eraser' | 'mixer' | RetouchTool;\n\nconst STROKE_TOOLS: readonly StrokeTool[] = ['brush', 'eraser', 'mixer', ...RETOUCH_TOOLS];\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 /** Overrides of the paint options' retouch settings (the retouching tools). */\n retouch?: Partial<RetouchSettings>;\n /**\n * Clone Stamp / Healing Brush: the source point for this stroke (document pixels). Given, it\n * becomes the clone source (as an Alt-click), so the stroke starts its offset afresh.\n */\n source?: { x: number; y: 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 * Clone Stamp / Healing Brush: the offset used (source − first stroke point, whole pixels) and\n * where the stroke's first point sampled.\n */\n clone?: { offset: { x: number; y: number }; source: { x: number; y: number }; aligned: boolean };\n /** Spot Healing: the offset of the patch it copied, or `null` when it filled smoothly. */\n spotHeal?: { offset: { x: number; y: number } | null };\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/** A stroke that paints dab by dab into live pixels (Mixer Brush, Smudge, Sponge). */\ninterface LiveDabStroke {\n push(points: readonly StrokePoint[]): CropBox[];\n finish(last?: StrokePoint): CropBox[];\n readonly bounds: CropBox | null;\n readonly dabCount: number;\n}\n\n/**\n * How a retouching stroke starts (see `PaintStrokeSession`): either coverage-based (Clone Stamp,\n * Healing Brush, Spot Healing, Dodge, Burn: `coverage` composited with `paint()` over the\n * pre-stroke pixels, optionally replaced by `finalPaint` when the stroke ends) or sequential\n * (`create`: dabs over live pixels, Smudge and Sponge).\n */\nexport type RetouchStrokeInit =\n | {\n kind: 'coverage';\n coverage: StrokeCoverage;\n apply: RetouchApply;\n /** Called with the first point before any dab is shown (the clone offset is fixed here). */\n start(first: StrokePoint): void;\n /** What the coverage lays while the stroke is drawn. */\n paint(): RetouchPaint;\n /** What it lays once the stroke ends, from the whole coverage (healing); `null` keeps `paint()`. */\n finalPaint?(coverage: CoverageMap): RetouchPaint | null;\n /** Extra result fields. */\n result?(): Partial<PaintStrokeResult>;\n }\n | {\n kind: 'dabs';\n /** Makes the stroke over the session's live pixels (RGBA, or an RGBA view of a mask). */\n create(pixels: PaintTileTarget): LiveDabStroke;\n result?(): Partial<PaintStrokeResult>;\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. Retouching strokes take one of the two shapes.\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: LiveDabStroke | null;\n private readonly mixerInit: MixerStrokeInit | null = null;\n private readonly retouch: RetouchStrokeInit | null = null;\n /** Writes a mask's RGBA view back (sequential retouching on a mask). */\n private readonly flushView: (() => void) | null = null;\n private started = false;\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 | RetouchStrokeInit,\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 if ('kind' in stroke && stroke.kind === 'coverage') {\n this.stroke = stroke.coverage;\n this.mixer = null;\n this.retouch = stroke;\n } else if ('kind' in stroke) {\n this.stroke = null;\n this.retouch = stroke;\n if (this.base.channels === 1) {\n const view = rgbaViewOfGrey(this.mixerPixels());\n this.mixer = stroke.create(view.target);\n this.flushView = view.flush;\n } else this.mixer = stroke.create(this.mixerPixels());\n } else {\n this.stroke = null;\n this.mixer = stroke.create(this.mixerPixels());\n this.mixerInit = stroke;\n }\n }\n\n /** The new tile of a coverage stroke over the pre-stroke tile `src`. */\n private compositeTile(src: Uint8ClampedArray, tx: number, ty: number, options: { into?: Uint8ClampedArray; region?: CropBox } = {}, paint?: RetouchPaint): Uint8ClampedArray {\n const r = this.retouch;\n if (r && r.kind === 'coverage') return applyRetouchToTile(this.stroke!.coverage, src, this.base.channels, tx, ty, paint ?? r.paint(), r.apply, options);\n return applyStrokeToTile(this.stroke!.coverage, src, this.base.channels, tx, ty, this.apply, options);\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 if (!this.started) {\n this.started = true;\n if (this.retouch?.kind === 'coverage') this.retouch.start(points[0]);\n }\n this.last = points[points.length - 1];\n const rects = this.mixer ? this.mixer.push(points) : this.stroke!.push(points);\n this.flushView?.();\n this.update(rects);\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 = this.compositeTile(src, tx, ty, 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 const rects = this.mixer ? this.mixer.finish(last) : this.stroke!.finish(last);\n this.flushView?.();\n this.update(rects);\n this.closed = true;\n const ch = this.base.channels;\n const r = this.retouch;\n if (r?.kind === 'coverage' && r.finalPaint) {\n // Healing: redo every touched tile from the pre-stroke pixels with the final paint.\n const final = r.finalPaint(this.stroke!.coverage);\n if (final) {\n for (const key of this.touched) {\n const { tx, ty } = tileCoord(key);\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n this.live.set(key, { data: this.compositeTile(src, tx, ty, {}, final), uniform: null });\n }\n }\n }\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.mixerInit ? this.mixerInit.end((this.mixer as MixerStroke).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 if (r?.result) Object.assign(result, r.result());\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 bitDepth?: BitDepth;\n colorProfile?: DocumentColorProfile;\n colorMode?: DocumentColorMode;\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 if (options.textShaper !== undefined) this.textShaperValue = options.textShaper;\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, retouch: { ...DEFAULT_RETOUCH }, cloneSource: null },\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 this.syncDisplay();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n private displayFor: [DocumentColorProfile | undefined, ProofSetup | null | undefined, DocumentColorMode | undefined] | null = null;\n\n /** Keeps the renderer's display transform in step with the document profile, mode and Proof Setup. */\n private syncDisplay(): void {\n const s = this.state;\n const d = this.displayFor;\n if (d && d[0] === s.colorProfile && d[1] === s.proofSetup && d[2] === s.colorMode) return;\n this.displayFor = [s.colorProfile, s.proofSetup, s.colorMode];\n try {\n this.renderer.setDisplayTransform(displayTransform(s.colorProfile, effectiveProof(s.proofSetup, s.colorMode)));\n } catch {\n this.renderer.setDisplayTransform(null);\n }\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 if (s.colorProfile) entry.colorProfile = s.colorProfile;\n if (s.colorMode) entry.colorMode = s.colorMode;\n if (s.bitDepth && s.bitDepth !== 8) entry.bitDepth = s.bitDepth;\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 if (entry.bitDepth) s.bitDepth = entry.bitDepth;\n else delete s.bitDepth;\n if (entry.colorProfile) s.colorProfile = entry.colorProfile;\n else delete s.colorProfile;\n if (entry.colorMode) s.colorMode = entry.colorMode;\n else delete s.colorMode;\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 || !hasSurface(layer)) 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 || !hasSurface(layer)) 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 * A tight base64 PNG data URL of a pixel or smart layer's content, or `null` for an unknown or\n * non-pixel layer, or when the layer is completely transparent.\n */\n public getLayerDataURL(id: string): string | null {\n const layer = this.getLayer(id);\n if (!layer || !hasSurface(layer)) return null;\n const surface = this.surfaceOf(layer);\n const enc = encodeSurface(surface);\n return enc ? enc.url : 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, smart object or shape composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer | SmartObjectLayer | ShapeLayer | TypeLayer): PixelSurface {\n if (layer.type !== 'pixel' || 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) => annotationOfLayer(l)?.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 if (layer.type === 'shape') {\n // A shape moves its paths: x, y is where the top-left of its geometry goes.\n const g = shapeGeometryBounds(layer) ?? { x: 0, y: 0 };\n const dx = move.x !== undefined ? move.x - g.x : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - g.y : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n requireUnlocked(layer, 'position', 'move it');\n let moved = offsetShape(layer, dx, dy, this.shapeEnv());\n const mask = layer.mask;\n if (mask && mask.linked !== false && !mask.source && (Math.round(dx) || Math.round(dy))) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, Math.round(dx), Math.round(dy)) } };\n this.commitLayers(mapLayer(this.state.layers, id, () => moved), undefined, commitToHistory);\n const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };\n return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };\n }\n if (layer.type === 'type') {\n // A type layer moves through its transform: x, y is where its text-space origin goes.\n const m = layer.transform ?? [1, 0, 0, 1, 0, 0];\n const dx = move.x !== undefined ? move.x - m[4] : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - m[5] : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n requireUnlocked(layer, 'position', 'move it');\n const mx = Math.round(dx);\n const my = Math.round(dy);\n let moved: TypeLayer = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };\n const mask = layer.mask;\n if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };\n this.commitLayers(mapLayer(this.state.layers, id, () => moved), undefined, commitToHistory);\n const t = moved.transform ?? [1, 0, 0, 1, 0, 0];\n return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, 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 shapeEnv: () => this.shapeEnv(),\n textEnv: () => this.textEnv(),\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 || session.typeLayer ? (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', pdf: 'application/pdf', ...BROWSER_IMAGE_TYPES };\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 const f = smartContentsFile(c);\n return { name: f.name, mimeType: f.name.endsWith('.png') ? 'image/png' : 'image/vnd.adobe.photoshop', bytes: f.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)) {\n if (l.type === 'smart' && !isContentsDecoded(l.contents) && l.contents.file && BROWSER_IMAGE_TYPES[l.contents.file.type]) pending.set(l.contents.id, l.contents);\n }\n const left: string[] = [];\n for (const [cid, c] of pending) {\n try {\n const url = `data:${BROWSER_IMAGE_TYPES[c.file!.type]};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 sources = this.renderer.compositorSources(width, height);\n const depth = this.bitDepth;\n if (depth !== 8) {\n // Merges keep the document's depth.\n const samples = workingToSamples(compositeDeep(layers, rect, sources, depth), depth);\n return surfaceFromDeepPixels({ width: rect.width, height: rect.height, data: samples }, 4, rect.x, rect.y);\n }\n const data = new LayerCompositor().composite(layers, rect, sources);\n return surfaceFromPixels({ width: rect.width, height: rect.height, data }, 4, rect.x, rect.y);\n }\n\n /**\n * The composite of the layer stack at the document's bit depth (as `compositePixels`, no\n * canvas): 16-bit sRGB samples, or 32-bit linear float, over `rect` (default the canvas). At 8\n * bits, the 8-bit composite widened to 16 bits.\n */\n public compositeDeepPixels(rect?: CropBox): { width: number; height: number; depth: 16 | 32; data: Uint16Array | Float32Array } {\n const { width, height } = this.naturalSize;\n const r = rect ?? { x: 0, y: 0, width, height };\n const depth = this.bitDepth;\n if (depth === 8) return { width: r.width, height: r.height, depth: 16, data: to16From8(this.compositePixels(r).data as Uint8ClampedArray) };\n const data = workingToSamples(compositeDeep(this.state.layers, r, this.compositorSources(), depth), depth);\n return { width: r.width, height: r.height, depth, data };\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 (hasSurface(layer)) 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) : hasSurface(layer) ? 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 /**\n * A small PNG data URL preview of a pixel or smart layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px; `null` for layers without pixels.\n */\n public getLayerThumbnailDataURL(id: string, options: { maxSide?: number; mask?: boolean } = {}): string | null {\n const thumb = this.getLayerThumbnail(id, options);\n if (!thumb) return null;\n return pngDataUrl(thumb.width, thumb.height, thumb.data, 4);\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' | 'retouch' | 'cloneSource'>> & {\n brush?: Partial<BrushSettings>;\n mixer?: Partial<MixerSettings>;\n retouch?: Partial<RetouchSettings>;\n }\n ): PaintOptions {\n const cur = this.state.paint;\n const retouch = options.retouch ? normalizeRetouch(options.retouch, cur.retouch) : cur.retouch;\n // Turning Aligned off (or on) starts the offset afresh at the next stroke.\n const cloneSource = cur.cloneSource && retouch.aligned !== cur.retouch.aligned ? { ...cur.cloneSource, offset: null } : cur.cloneSource;\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 retouch,\n cloneSource,\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 /**\n * Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document\n * pixels. The next stroke fixes the offset from its first point. View state, no undo.\n */\n public setCloneSource(point: { x: number; y: number } | null): CloneSource | null {\n if (point && (!Number.isFinite(point.x) || !Number.isFinite(point.y))) throw new LayerOperationError('The clone source needs finite x and y (document pixels).');\n const source = point ? { x: Math.round(point.x), y: Math.round(point.y), offset: null } : null;\n this.state.paint = { ...this.state.paint, cloneSource: source };\n this.notify();\n return source;\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 === 'shape') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a shape layer; its pixels are drawn from its paths. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);\n if (layer.type === 'type') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a type layer; its pixels are drawn from its text. Rasterize it first (rasterize_layer), or ${action} on its mask (channel 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 (!STROKE_TOOLS.includes(tool)) throw new LayerOperationError(`Unknown paint tool \"${String(tool)}\"; use one of ${STROKE_TOOLS.join(', ')}.`);\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : isRetouchTool(tool) ? 'retouch' : '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 if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply);\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** A retouching stroke's start: its settings, what it samples and how it composites. */\n private retouchStroke(tool: RetouchTool, init: StrokeInit, brush: BrushSettings, target: ReturnType<ImageEditorController['paintTarget']>, apply: StrokeApply): RetouchStrokeInit {\n const settings = normalizeRetouch(init.retouch ?? {}, this.state.paint.retouch);\n const mask = target.channel !== 'pixels';\n if (tool === 'sponge' && mask) throw new LayerOperationError('The Sponge changes saturation, and a mask has none; target pixels (channel pixels), or use dodge or burn on the mask.');\n if (tool === 'smudge') return { kind: 'dabs', create: (pixels) => new RetouchDabStroke(brush, pixels, smudgeEffect(brush, settings.strength)) };\n if (tool === 'sponge') return { kind: 'dabs', create: (pixels) => new RetouchDabStroke(brush, pixels, spongeEffect(brush, settings.spongeMode === 'saturate', settings.vibrance)) };\n const retouchApply: RetouchApply = {\n opacity: tool === 'clone' || tool === 'heal' ? brush.opacity : 1,\n blend: tool === 'clone' && !mask ? brush.blend : 'normal',\n lockTransparency: target.lockTransparency,\n selection: apply.selection,\n offset: target.offset,\n };\n const coverage = new StrokeCoverage(brush);\n if (tool === 'dodge' || tool === 'burn') {\n const paint: RetouchPaint = { kind: 'tone', tone: toneOf(tool, settings) };\n return { kind: 'coverage', coverage, apply: retouchApply, start: () => {}, paint: () => paint };\n }\n // Masks sample only themselves; pixels sample the layer or the composite.\n const sample = mask ? 'current' : settings.sample;\n const own = this.surfaceSource(target.surface, target.offset);\n const source = sample === 'current' ? own : this.compositeSource(sample === 'all' ? null : target.id);\n const pad = (r: CropBox, m: number): CropBox => ({ x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m });\n const covered = (cov: CoverageMap, r: CropBox) => {\n const m = new Uint8Array(r.width * r.height);\n for (let y = 0; y < r.height; y++) for (let x = 0; x < r.width; x++) m[y * r.width + x] = cov.get(r.x + x, r.y + y) > 0 ? 1 : 0;\n return m;\n };\n if (tool === 'spot-heal') {\n const { width, height } = this.naturalSize;\n let patch: { x: number; y: number } | null = null;\n const trail: RetouchPaint = { kind: 'color', color: [0, 0, 0, 96] };\n return {\n kind: 'coverage',\n coverage,\n apply: retouchApply,\n start: () => {},\n paint: () => trail,\n finalPaint: (cov) => {\n if (!cov.bounds) return null;\n const win = spotHealWindow(cov.bounds, { width, height });\n if (win.width <= 0 || win.height <= 0) return null;\n const img = readSourceRegion(source, win);\n const hole = covered(cov, win);\n const healed = spotHealPixels(win.width, win.height, img, hole, brush.size, settings.spotType, (off) => {\n patch = off ? { x: off[0], y: off[1] } : null;\n });\n return { kind: 'source', source: new RegionPixelSource(win, healed), dx: 0, dy: 0 };\n },\n result: () => ({ spotHeal: { offset: patch } }),\n };\n }\n // Clone Stamp and Healing Brush: the offset from the source point.\n if (init.source) this.setCloneSource(init.source);\n const cs = this.state.paint.cloneSource;\n if (!cs) throw new LayerOperationError(`The ${tool === 'clone' ? 'Clone Stamp' : 'Healing Brush'} needs a source point; Alt-click the canvas, call set_clone_source, or pass source { x, y }.`);\n let offset = { x: 0, y: 0 };\n let first = { x: 0, y: 0 };\n const paint = (): RetouchPaint => ({ kind: 'source', source, dx: offset.x, dy: offset.y });\n return {\n kind: 'coverage',\n coverage,\n apply: retouchApply,\n start: (p) => {\n first = { x: Math.round(p.x), y: Math.round(p.y) };\n offset = settings.aligned && cs.offset ? { ...cs.offset } : { x: cs.x - first.x, y: cs.y - first.y };\n if (settings.aligned) this.state.paint = { ...this.state.paint, cloneSource: { x: cs.x, y: cs.y, offset: { ...offset } } };\n },\n paint,\n finalPaint:\n tool === 'heal'\n ? (cov) => {\n if (!cov.bounds) return null;\n const g = pad(cov.bounds, 2);\n const src = readSourceRegion(source, g, offset.x, offset.y);\n // The texture is fitted to what is seen: the layer, or the composite when sampling several layers.\n const dst = readSourceRegion(source, g);\n return { kind: 'source', source: new RegionPixelSource(g, healPixels(g.width, g.height, src, dst, covered(cov, g))), dx: 0, dy: 0 };\n }\n : undefined,\n result: () => ({ clone: { offset: { ...offset }, source: { x: first.x + offset.x, y: first.y + offset.y }, aligned: settings.aligned } }),\n };\n }\n\n /** A pixel source over a surface (layer pixels or a mask) in layer space at `offset`. */\n private surfaceSource(surface: TiledSurface, offset: { x: number; y: number }): PixelSource {\n return new TilePixelSource((tx, ty) => surface.tiles.get(tileKey(tx, ty))?.data ?? (surface.defaultValue ? defaultTile(surface) : null), surface.channels, offset);\n }\n\n /** A pixel source over the composite of the current layers (through `throughId` and below, or all of them), as it is now. */\n private compositeSource(throughId: string | null): PixelSource {\n const stack = withoutAnnotations(this.state.layers);\n const layers = throughId ? layersThrough(stack, throughId) : stack;\n const compositor = new LayerCompositor();\n const sources = this.compositorSources();\n return new TilePixelSource((tx, ty) => compositor.tile(layers, tx, ty, sources), 4);\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 /** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */\n private annotationLayer(id: string): AnnotationLayer | ShapeLayer | TypeLayer | undefined {\n return flattenLayers(this.state.layers).find((l): l is AnnotationLayer | ShapeLayer | TypeLayer => (l.type === 'annotation' || l.type === 'shape' || l.type === 'type') && annotationOfLayer(l)?.id === id);\n }\n\n /** What migrating an annotation needs: the canvas size, patterns and the text shaper. */\n private annotationEnv(): ShapeEnv & TextEnv {\n return { ...this.shapeEnv(), ...this.textEnv() };\n }\n\n /**\n * Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,\n * rectangle, ellipse, line and arrow annotations become shape layers that remember the\n * annotation (so `annotations` and `updateAnnotation` keep working); text and images stay\n * annotation layers.\n */\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, this.annotationEnv());\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 const current = layer ? annotationOfLayer(layer) : null;\n if (layer && current) {\n const annotation = { ...current, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(current) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => this.layerForAnnotation(layer, annotation, name));\n }\n this.notify();\n }\n\n /** `layer` (an annotation, or a shape or type layer migrated from one) showing `annotation` instead, keeping its other properties. */\n private layerForAnnotation(layer: AnnotationLayer | ShapeLayer | TypeLayer, annotation: Annotation, name: string): ImageLayer {\n const fresh = annotationLayerFor(annotation, this.annotationEnv());\n const keep: Partial<ImageLayer> = { id: layer.id, visible: layer.visible, opacity: layer.opacity };\n for (const k of ['clipped', 'mask', 'locks', 'effects'] as const) if (layer[k] !== undefined) (keep as Record<string, unknown>)[k] = layer[k];\n if (fresh.type !== 'annotation' && layer.type !== 'annotation' && layer.fillOpacity !== undefined) (keep as { fillOpacity?: number }).fillOpacity = layer.fillOpacity;\n // An annotation layer's own blend (set on the layer) wins over the annotation's.\n const blend = fresh.type === 'annotation' ? (layer.type === 'annotation' ? layer.blend : 'normal') : layer.type === 'annotation' && layer.blend !== 'normal' ? layer.blend : fresh.blend;\n return { ...fresh, ...keep, name, blend } as ImageLayer;\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 /** Deletes every annotation layer and every shape layer that still is the annotation it was migrated from. */\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation' || ((l.type === 'shape' || l.type === 'type') && l.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 const url = canvas.toDataURL(format, quality);\n if (!this.state.colorProfile) return url;\n // A document in another colour space carries its profile in the file.\n const parsed = parseDataUrl(url);\n if (!parsed) return url;\n const bytes = this.withEmbeddedProfile(parsed.bytes, parsed.mime);\n return bytes === parsed.bytes ? url : `data:${parsed.mime};base64,${bytesToBase64(bytes)}`;\n }\n\n /** PNG or JPEG bytes with the document's ICC profile embedded (other formats unchanged). */\n private withEmbeddedProfile(bytes: Uint8Array, mime: string): Uint8Array {\n const icc = profileBytes(this.state.colorProfile);\n try {\n if (mime === 'image/png') return writePngIcc(bytes, icc, this.getColorProfile().name);\n if (mime === 'image/jpeg') return writeJpegIcc(bytes, icc);\n } catch {\n // Not the expected container (a mocked canvas): leave it.\n }\n return bytes;\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 const tag = async (blob: Blob): Promise<Blob> => {\n if (!this.state.colorProfile || typeof blob.arrayBuffer !== 'function') return blob;\n const bytes = new Uint8Array(await blob.arrayBuffer());\n const out = this.withEmbeddedProfile(bytes, blob.type || format);\n return out === bytes ? blob : new Blob([out as BlobPart], { type: blob.type || format });\n };\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) tag(blob).then(resolve, reject);\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 tag(new Blob([ab], { type: format })).then(resolve, reject);\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 // --- Type layers ---\n\n private textShaperValue: TextShaper | null | undefined;\n\n /** The shaper that lays out and draws type layers (`null` when there is none here). */\n public getTextShaper(): TextShaper | null {\n if (this.textShaperValue === undefined) this.textShaperValue = createCanvasTextShaper({ createCanvas: (w, h) => this.renderer.createCanvas(w, h) });\n return this.textShaperValue;\n }\n\n /** Sets the text shaper (fonts) type layers lay out and draw with; existing layers redraw on their next edit. */\n public setTextShaper(shaper: TextShaper | null): void {\n this.textShaperValue = shaper;\n this.notify();\n }\n\n /** What type layers lay out and draw with. */\n private textEnv(): TextEnv {\n return { ...this.naturalSize, shaper: this.getTextShaper(), createCanvas: (w, h) => this.renderer.createCanvas(w, h) };\n }\n\n /** Font families the shaper offers (generic families at least). */\n public listFonts(): string[] {\n return this.getTextShaper()?.families?.() ?? ['sans-serif', 'serif', 'monospace'];\n }\n\n private typeInfo(id: string): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const ranges = <S>(runs: ReadonlyArray<{ length: number; style: S }>) => {\n let at = 0;\n return runs.map((r) => {\n const out = { start: at, end: at + r.length, style: JSON.parse(JSON.stringify(r.style)) as S };\n at += r.length;\n return out;\n });\n };\n const shaper = this.getTextShaper();\n let lines: TypeLayerInfo['lines'] = [];\n let missing = layer.missingFonts ?? [];\n if (shaper) {\n const layout = layoutText(layer, shaper);\n const r = (n: number) => Math.round(n * 1000) / 1000;\n lines = layout.lines.map((l) => ({ start: l.start, end: l.end, baseline: r(l.baseline), width: r(l.width) }));\n if (layer.rendering === 'stored') missing = layout.missingFonts;\n }\n const info: TypeLayerInfo = {\n id: layer.id,\n name: layer.name,\n text: layer.text,\n runs: ranges(layer.runs),\n paragraphs: ranges(layer.paragraphs),\n frame: { ...layer.frame },\n transform: [...(layer.transform ?? [1, 0, 0, 1, 0, 0])] as unknown as AffineMatrix,\n lines,\n bounds: this.getLayerBounds(id),\n missingFonts: [...missing],\n rendering: layer.rendering === 'stored' ? 'stored' : 'live',\n };\n if (layer.annotation) info.annotationId = layer.annotation.id;\n return info;\n }\n\n /** A type layer's text, runs, paragraphs, frame, lines and bounds. */\n public getTextLayer(id: string): TypeLayerInfo {\n return this.typeInfo(id);\n }\n\n /**\n * Adds a type layer (one undo step) and makes it active: point text at `x`, `y`, or paragraph\n * text wrapped in `box`. Refused without a text shaper. Returns its id.\n */\n public addTextLayer(init: TextLayerInit): string {\n const text = normalizeText(init.text);\n if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError('x and y must be numbers (document pixels).');\n const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, (init.style ?? {}) as Record<string, unknown>);\n const paragraph = patchParagraphStyle({}, (init.paragraph ?? {}) as Record<string, unknown>);\n let frame: TypeLayer['frame'] = { kind: 'point' };\n if (init.box) {\n const { width, height } = init.box;\n if (!(Number.isFinite(width) && width >= 1 && Number.isFinite(height) && height >= 1)) throw new LayerOperationError('box.width and box.height must be at least 1 pixel.');\n frame = { kind: 'box', width, height };\n }\n if (init.rotation !== undefined && !Number.isFinite(init.rotation)) throw new LayerOperationError('rotation must be a number of degrees.');\n if (init.opacity !== undefined && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be between 0 and 1.');\n if (init.blend !== undefined && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode \"${String(init.blend)}\".`);\n const t: AffineMatrix = [1, 0, 0, 1, init.x, init.y];\n const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;\n const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());\n if (init.opacity !== undefined) layer.opacity = init.opacity;\n if (init.blend !== undefined) layer.blend = init.blend;\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 * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at\n * `start`. One undo step unless `commitToHistory` is false (typing). Returns the layer.\n */\n public setText(id: string, text: string, range: { start?: number; end?: number } = {}, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const insert = normalizeText(text);\n const [start, end] = textRange(layer.text, range.start, range.end);\n const { layers } = updateTypeLayer(this.state.layers, id, (l) => replaceTextRange(l, start, end, insert), this.textEnv(), 'edit its text');\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /** Sets character style fields over [start, end) (default all), one undo step. Returns the layer. */\n public setTextStyle(id: string, style: Partial<TextStyle> & Record<string, unknown>, range: { start?: number; end?: number } = {}, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const [start, end] = textRange(layer.text, range.start, range.end);\n if (!Object.keys(style).length) throw new LayerOperationError('Give at least one style field to set.');\n // Validate once before touching the runs.\n patchTextStyle(DEFAULT_TEXT_STYLE, style);\n const { layers } = updateTypeLayer(\n this.state.layers,\n id,\n (l) => {\n if (start === end) {\n // An empty range styles the whole of an empty text (the caret's style).\n if (l.text.length) throw new LayerOperationError('The range is empty; give start < end (or leave both out for the whole text).');\n return { runs: [{ length: 0, style: patchTextStyle(styleAt(l.runs, 0, DEFAULT_TEXT_STYLE), style) }] };\n }\n return { runs: restyleRange(l.runs, start, end, (s) => patchTextStyle(s, style)) as TypeLayer['runs'] };\n },\n this.textEnv(),\n 'restyle its text'\n );\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /** Sets paragraph style fields on every paragraph that [start, end) touches (default all), one undo step. */\n public setParagraphStyle(id: string, style: ParagraphStyle & Record<string, unknown>, range: { start?: number; end?: number } = {}, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n const [start, end] = textRange(layer.text, range.start, range.end);\n if (!Object.keys(style).length) throw new LayerOperationError('Give at least one paragraph field to set.');\n patchParagraphStyle({}, style);\n const { layers } = updateTypeLayer(\n this.state.layers,\n id,\n (l) => {\n let at = 0;\n const paragraphs = l.paragraphs.map((p) => {\n const a = at;\n at += p.length;\n const touches = (a < end && at > start) || (start === end && start >= a && (start < at || at === l.text.length));\n return touches || !l.text.length ? { length: p.length, style: patchParagraphStyle(p.style, style) } : p;\n });\n return { paragraphs };\n },\n this.textEnv(),\n 'restyle its paragraphs'\n );\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /**\n * Point text ↔ paragraph text: `{ kind: 'box', width, height }` wraps the text in a box (its\n * top-left where the first line's top-left is now), `{ kind: 'point' }` sets it on one line per\n * paragraph. Also resizes a box. One undo step.\n */\n public setTextFrame(id: string, frame: { kind: 'point' } | { kind: 'box'; width: number; height: number }, commitToHistory = true): TypeLayerInfo {\n const layer = requireTypeLayer(this.state.layers, id);\n if (frame.kind === 'box' && !(Number.isFinite(frame.width) && frame.width >= 1 && Number.isFinite(frame.height) && frame.height >= 1)) throw new LayerOperationError('width and height must be at least 1 pixel.');\n if (frame.kind !== 'box' && frame.kind !== 'point') throw new LayerOperationError('kind must be point or box.');\n const shaper = this.getTextShaper();\n const { layers } = updateTypeLayer(\n this.state.layers,\n id,\n (l) => {\n if (frame.kind === 'box' && l.frame.kind === 'box') return { frame: { kind: 'box', width: frame.width, height: frame.height } };\n if (!shaper) throw new LayerOperationError('Changing between point and paragraph text needs a text shaper to measure the text.');\n const lines = layoutText(l, shaper).lines;\n const first = lines[0];\n // Keep the first line where it is: shift the text-space origin.\n const dx = frame.kind === 'box' ? (first ? first.x : 0) : 0;\n const dy = frame.kind === 'box' ? (first ? first.baseline - first.ascent : 0) : first ? first.baseline : 0;\n const m = textMatrix(l);\n const shifted: AffineMatrix = [m[0], m[1], m[2], m[3], m[0] * dx + m[2] * dy + m[4], m[1] * dx + m[3] * dy + m[5]];\n const nextFrame: TypeLayer['frame'] = frame.kind === 'box' ? { kind: 'box', width: frame.width, height: frame.height } : { kind: 'point' };\n return { frame: nextFrame, transform: shifted };\n },\n this.textEnv(),\n 'change its frame'\n );\n this.commitLayers(layers, undefined, commitToHistory);\n return this.typeInfo(id);\n }\n\n /**\n * Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and\n * layer properties (needs a shaper that gives outlines). The fill is the first run's colour;\n * `warnings` says when other colours were dropped. One undo step.\n */\n public convertTextToShape(id: string): { layerId: string; warnings: string[] } {\n const layer = requireTypeLayer(this.state.layers, id);\n requireUnlocked(layer, 'all', 'convert it');\n const shaper = this.getTextShaper();\n if (!shaper) throw new LayerOperationError('Convert to Shape needs a text shaper that gives glyph outlines; none is set here.');\n const parts = textOutlines(layer, shaper);\n const subpaths = parts.flatMap((p) => bezierToSubpaths(p.path));\n if (!subpaths.length) throw new LayerOperationError('The text draws nothing (it is empty or only spaces), so there is no shape to make.');\n const warnings: string[] = [];\n const colors = new Set(layer.runs.filter((r) => r.length > 0).map((r) => `${r.style.color}/${r.style.opacity ?? 1}`));\n if (colors.size > 1) warnings.push('The text had several colours; the shape takes the first one.');\n const missing = layoutText(layer, shaper).missingFonts;\n if (missing.length) warnings.push(`Missing fonts were drawn with a fallback: ${missing.join(', ')}.`);\n const first = layer.runs.find((r) => r.length > 0)?.style ?? DEFAULT_TEXT_STYLE;\n const fill: ShapePaint = first.opacity !== undefined && first.opacity !== 1 ? { type: 'color', color: first.color, opacity: first.opacity } : { type: 'color', color: first.color };\n const shape = createShapeLayer({ components: [{ op: 'combine', subpaths }], fill, stroke: null, name: layer.name }, this.shapeEnv(), layer.id);\n const keep: Partial<ShapeLayer> = { visible: layer.visible, opacity: layer.opacity, blend: layer.blend };\n for (const k of ['clipped', 'mask', 'locks', 'effects', 'fillOpacity'] as const) if (layer[k] !== undefined) (keep as Record<string, unknown>)[k] = layer[k];\n this.commitLayers(mapLayer(this.state.layers, id, () => ({ ...shape, ...keep }) as ShapeLayer));\n return { layerId: id, warnings };\n }\n\n /** The layout of a type layer (lines, characters, bounds in text space), or `null` without a shaper. */\n public getTextLayout(id: string): TextLayout | null {\n const layer = requireTypeLayer(this.state.layers, id);\n const shaper = this.getTextShaper();\n return shaper ? layoutText(layer, shaper) : null;\n }\n\n /** The text index nearest a document point (for clicks on a type layer). */\n public textIndexAt(id: string, point: ImagePoint): number {\n const layer = requireTypeLayer(this.state.layers, id);\n const layout = this.getTextLayout(id);\n return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;\n }\n\n /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */\n public textCaret(id: string, index: number): { top: ImagePoint; bottom: ImagePoint; line: number } | null {\n const layer = requireTypeLayer(this.state.layers, id);\n const layout = this.getTextLayout(id);\n if (!layout) return null;\n const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));\n const m = textMatrix(layer);\n const at = (x: number, y: number) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });\n return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };\n }\n\n // --- Shape layers ---\n\n /** What shape rendering needs: the canvas size and the document's patterns. */\n private shapeEnv(): ShapeEnv {\n return { ...this.naturalSize, pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null };\n }\n\n /** One component from tool or UI input (validated). */\n private componentFrom(spec: ShapeGeometryInput, path: string): ShapeComponent {\n const op = requireOperation(spec.op, `${path}op`);\n if (spec.shape !== undefined && spec.subpaths !== undefined) throw new LayerOperationError(`Give ${path}shape or ${path}subpaths, not both.`);\n if (spec.shape !== undefined) return liveComponent(normalizeLiveShape(spec.shape, `${path}shape`), op);\n if (spec.subpaths !== undefined) return { op, subpaths: normalizeSubpaths(spec.subpaths, `${path}subpaths`) };\n throw new LayerOperationError(`${path}shape (a live shape: { kind: rect | ellipse | polygon | star | line, … }) or ${path}subpaths (anchors or SVG path data) is required.`);\n }\n\n private shapeInfo(id: string): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const r = (n: number) => Math.round(n * 1000) / 1000;\n const box = (b: CropBox | null) => (b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null);\n const info: ShapeLayerInfo = {\n id: layer.id,\n name: layer.name,\n components: layer.components.map((c, index) => ({\n index,\n op: c.op,\n live: c.live ? JSON.parse(JSON.stringify(c.live)) : null,\n subpaths: roundSubpaths(c.subpaths),\n bounds: box(shapeGeometryBounds({ components: [c] })),\n })),\n fillRule: layer.fillRule ?? 'nonzero',\n fill: layer.fill ? JSON.parse(JSON.stringify(layer.fill)) : null,\n stroke: layer.stroke ? JSON.parse(JSON.stringify(layer.stroke)) : null,\n geometryBounds: box(shapeGeometryBounds(layer)),\n bounds: this.getLayerBounds(id),\n };\n if (layer.annotation) info.annotationId = layer.annotation.id;\n return info;\n }\n\n /** A shape layer's components, fill, stroke and bounds. */\n public getShapeLayer(id: string): ShapeLayerInfo {\n return this.shapeInfo(id);\n }\n\n /**\n * Adds a shape layer (one undo step) and makes it active: a live shape or subpaths (or several\n * `components`), filled with `fill` (default the foreground colour, except for a stroke-only\n * open path) and stroked with `stroke`. Goes above the active layer unless placed. Returns its id.\n */\n public addShapeLayer(init: ShapeLayerInit): string {\n const specs: ShapeGeometryInput[] = init.components?.length ? init.components : [init];\n if (init.components?.length && (init.shape !== undefined || init.subpaths !== undefined)) throw new LayerOperationError('Give components, or shape / subpaths, not both.');\n const components = specs.map((c, i) => this.componentFrom(c, init.components?.length ? `components[${i}].` : ''));\n const stroke = init.stroke ? normalizeShapeStroke(init.stroke, 'stroke') : null;\n const openOnly = components.every((c) => !c.live && c.subpaths.every((sp) => !sp.closed));\n const fill = init.fill === undefined ? (stroke && openOnly ? null : normalizeShapePaint(this.state.paint.foreground, 'fill')) : init.fill === null ? null : normalizeShapePaint(init.fill, 'fill');\n if (!fill && !stroke) throw new LayerOperationError('A shape with neither fill nor stroke draws nothing; give fill or stroke.');\n if (init.fillRule !== undefined && init.fillRule !== 'nonzero' && init.fillRule !== 'evenodd') throw new LayerOperationError('fillRule must be nonzero or evenodd.');\n if (init.opacity !== undefined && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be between 0 and 1.');\n if (init.blend !== undefined && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode \"${String(init.blend)}\".`);\n const layer = createShapeLayer({ components, fill, stroke, fillRule: init.fillRule, name: init.name, opacity: init.opacity, blend: init.blend }, this.shapeEnv());\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 * Sets a shape's fill, stroke or fill rule (one undo step unless `commitToHistory` is false):\n * `fill` / `stroke` replace them (`null` removes); `stroke` given as a partial object changes\n * only those settings of the current stroke. Returns the shape.\n */\n public setShapeStyle(\n id: string,\n changes: { fill?: ShapePaint | string | null; stroke?: ShapeStrokeInput | null; fillRule?: ShapeFillRule },\n commitToHistory = true\n ): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const fill = changes.fill === undefined ? layer.fill : changes.fill === null ? null : normalizeShapePaint(changes.fill, 'fill');\n const stroke = changes.stroke === undefined ? layer.stroke : changes.stroke === null ? null : normalizeShapeStroke(changes.stroke, 'stroke', layer.stroke);\n if (changes.fillRule !== undefined && changes.fillRule !== 'nonzero' && changes.fillRule !== 'evenodd') throw new LayerOperationError('fillRule must be nonzero or evenodd.');\n const fillRule = changes.fillRule ?? layer.fillRule ?? 'nonzero';\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ fill, stroke, fillRule: fillRule === 'nonzero' ? undefined : fillRule }), this.shapeEnv(), 'restyle it');\n this.commitLayers(layers, undefined, commitToHistory);\n return this.shapeInfo(id);\n }\n\n private componentIndex(layer: ShapeLayer, index: unknown): number {\n if (!Number.isInteger(index) || (index as number) < 0 || (index as number) >= layer.components.length) {\n throw new LayerOperationError(`componentIndex must be 0..${layer.components.length - 1} (get_shape_layer lists the components), got ${String(index)}.`);\n }\n return index as number;\n }\n\n /**\n * Changes one component (one undo step unless `commitToHistory` is false): its live shape\n * (fields not given keep their values when the kind stays the same), its subpaths (which drops\n * the live shape) or its operation. Returns the shape.\n */\n public setShapeComponent(id: string, index: number, change: ShapeGeometryInput, commitToHistory = true): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const i = this.componentIndex(layer, index);\n const current = layer.components[i];\n let shape = change.shape;\n if (shape && current.live && (shape as { kind?: unknown }).kind === undefined) shape = { ...current.live, ...shape } as LiveShape;\n else if (shape && current.live && (shape as { kind?: unknown }).kind === current.live.kind) shape = { ...current.live, ...shape } as LiveShape;\n const next = shape !== undefined || change.subpaths !== undefined ? this.componentFrom({ ...change, shape, op: change.op ?? current.op }, '') : { ...current, op: requireOperation(change.op) };\n const components = layer.components.map((c, k) => (k === i ? next : c));\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers, undefined, commitToHistory);\n return this.shapeInfo(id);\n }\n\n /** Adds a component to a shape (path operations), on top unless `index` is given (one undo step). Returns the shape. */\n public addShapeComponent(id: string, spec: ShapeGeometryInput, index?: number): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const c = this.componentFrom(spec, '');\n const at = index === undefined ? layer.components.length : index;\n if (!Number.isInteger(at) || at < 0 || at > layer.components.length) throw new LayerOperationError(`index must be 0..${layer.components.length}, got ${String(index)}.`);\n const components = [...layer.components.slice(0, at), c, ...layer.components.slice(at)];\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers);\n return this.shapeInfo(id);\n }\n\n /** Removes one component of a shape (one undo step); the last one cannot go (delete the layer instead). Returns the shape. */\n public removeShapeComponent(id: string, index: number): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n const i = this.componentIndex(layer, index);\n if (layer.components.length === 1) throw new LayerOperationError('This is the shape\\'s only component; delete the layer instead (delete_layer).');\n const components = layer.components.filter((_, k) => k !== i);\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers);\n return this.shapeInfo(id);\n }\n\n /**\n * Merge Shape Components: the area the components combine to becomes one plain component (live\n * shapes become paths), as one undo step. Returns the shape.\n */\n public mergeShapeComponents(id: string): ShapeLayerInfo {\n const layer = requireShapeLayer(this.state.layers, id);\n if (layer.components.length < 2) throw new LayerOperationError('The shape has one component; there is nothing to merge.');\n const components = mergeShapeComponentsOp(layer.components, layer.fillRule);\n const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components, fillRule: undefined }), this.shapeEnv(), 'edit its path');\n this.commitLayers(layers);\n return this.shapeInfo(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 if (layer.type === 'shape' || layer.type === 'type') {\n next = layer.type === 'shape' ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);\n if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;\n } else {\n throw new LayerOperationError(`Only shape, type and 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 // --- Colour management ---\n\n /** The document's colour profile: its name, built-in id (or `null` for an embedded profile), colour space, and Image › Mode. */\n public getColorProfile(): {\n name: string;\n builtin: BuiltinProfileId | null;\n colorSpace: ColorSpace;\n srgb: boolean;\n mode: 'rgb' | 'cmyk' | 'lab';\n cmykProfile?: { name: string; builtin: BuiltinProfileId | null; intent: Intent };\n } {\n const doc = this.state.colorProfile;\n const p = resolveProfile(doc);\n const m = this.state.colorMode;\n const out: ReturnType<ImageEditorController['getColorProfile']> = { name: doc?.name ?? p.description, builtin: doc ? (doc.builtin ?? null) : 'srgb', colorSpace: p.colorSpace, srgb: isSrgb(doc), mode: m?.mode ?? 'rgb' };\n if (m?.mode === 'cmyk') out.cmykProfile = { name: m.profile.name, builtin: m.profile.builtin ?? null, intent: m.intent };\n return out;\n }\n\n /**\n * Image › Mode (one undo step): `rgb`, `cmyk` (with a CMYK `profile`, default the coated\n * CMYK, and the `intent` RGB is separated with) or `lab`. The pixels stay RGB in the working\n * space; a CMYK document is soft-proofed through its profile in the viewport, and PSD export\n * writes CMYK or Lab channels. Returns the profile summary.\n */\n public setColorMode(mode: 'rgb' | 'cmyk' | 'lab', options: { profile?: string | Uint8Array; intent?: Intent } = {}): ReturnType<ImageEditorController['getColorProfile']> {\n let next: DocumentColorMode | null;\n if (mode === 'rgb') {\n if (options.profile !== undefined) throw new LayerOperationError('An RGB document takes no CMYK profile; use assign_profile for its working space.');\n next = null;\n } else if (mode === 'lab') next = { mode: 'lab' };\n else if (mode === 'cmyk') {\n let doc: DocumentColorProfile;\n try {\n doc = documentProfile(options.profile ?? 'coated-cmyk');\n } catch (e) {\n throw new LayerOperationError(e instanceof Error ? e.message : String(e));\n }\n const cs = resolveProfile(doc).colorSpace;\n if (cs !== 'cmyk') throw new LayerOperationError(`CMYK mode needs a CMYK profile; \"${doc.name}\" is ${cs.toUpperCase()}.`);\n const intent = options.intent ?? 'relative';\n if (!INTENTS.includes(intent)) throw new LayerOperationError(`Unknown rendering intent \"${String(intent)}\"; use ${INTENTS.join(', ')}.`);\n next = { mode: 'cmyk', profile: doc, intent };\n } else throw new LayerOperationError(`Unknown colour mode \"${String(mode)}\"; use rgb, cmyk or lab.`);\n this.recordHistory();\n if (next) this.state.colorMode = next;\n else delete this.state.colorMode;\n this.notify();\n return this.getColorProfile();\n }\n\n /** Resolves a profile argument (built-in id or alias, or ICC bytes) as an RGB document profile. */\n private rgbProfileArg(profile: string | Uint8Array, action: string): DocumentColorProfile {\n let doc: DocumentColorProfile;\n try {\n doc = documentProfile(profile);\n } catch (e) {\n throw new LayerOperationError(e instanceof Error ? e.message : String(e));\n }\n const cs = resolveProfile(doc).colorSpace;\n if (cs !== 'rgb') throw new LayerOperationError(`${action} needs an RGB profile; \"${doc.name}\" is ${cs.toUpperCase()}. The document is RGB: soft-proof CMYK with set_proof_setup, or export to it.`);\n return doc;\n }\n\n /**\n * Edit › Assign Profile (one undo step): the document's numbers now mean colours in\n * `profile` (a built-in id such as `display-p3`, or ICC bytes); no pixel changes, so colours\n * look different unless the profiles match. Returns the new profile.\n */\n public assignProfile(profile: string | Uint8Array): ReturnType<ImageEditorController['getColorProfile']> {\n const doc = this.rgbProfileArg(profile, 'Assign Profile');\n this.recordHistory();\n if (doc.builtin === 'srgb') delete this.state.colorProfile;\n else this.state.colorProfile = doc;\n this.notify();\n return this.getColorProfile();\n }\n\n /**\n * Edit › Convert to Profile (one undo step): every pixel layer's numbers are converted so its\n * colours stay the same in `profile` (with `intent` and black point compensation), and\n * `profile` becomes the document's. Shape and type layers, layer effects and adjustment layers\n * keep their numbers (they are listed in `unconvertedLayerIds`). Returns the profile and the\n * layers changed.\n */\n public convertToProfile(\n profile: string | Uint8Array,\n options: { intent?: Intent; bpc?: boolean } = {}\n ): { profile: ReturnType<ImageEditorController['getColorProfile']>; convertedLayerIds: string[]; unconvertedLayerIds: string[] } {\n const doc = this.rgbProfileArg(profile, 'Convert to Profile');\n const intent = options.intent ?? 'relative';\n if (!INTENTS.includes(intent)) throw new LayerOperationError(`Unknown rendering intent \"${String(intent)}\"; use ${INTENTS.join(', ')}.`);\n const t = Transform.create(resolveProfile(this.state.colorProfile), resolveProfile(doc), { intent, bpc: options.bpc ?? true });\n const converted: string[] = [];\n const unconverted: string[] = [];\n const walk = (layers: readonly ImageLayer[]): ImageLayer[] =>\n layers.map((l) => {\n if (l.type === 'group') return { ...l, children: walk(l.children) };\n if (l.type === 'pixel' && l.source !== 'document') {\n converted.push(l.id);\n return { ...l, surface: convertSurfaceColors(l.surface, t) };\n }\n if (l.type === 'pixel' && l.source === 'document') {\n converted.push(l.id);\n return { ...l, surface: convertSurfaceColors(this.surfaceOf(l), t), source: undefined } as ImageLayer;\n }\n if (l.type === 'shape' || l.type === 'type' || l.type === 'smart' || l.effects?.items.length) unconverted.push(l.id);\n return l;\n });\n const next = walk(this.state.layers);\n this.commitLayers(next, () => {\n if (doc.builtin === 'srgb') delete this.state.colorProfile;\n else this.state.colorProfile = doc;\n });\n return { profile: this.getColorProfile(), convertedLayerIds: converted, unconvertedLayerIds: unconverted };\n }\n\n /**\n * View › Proof Setup (view state, not an undo step): shows the document as `profile` would\n * reproduce it, or turns proofing off with `null`. Returns the setup.\n */\n public setProofSetup(setup: (Partial<Omit<ProofSetup, 'profile'>> & { profile: string | Uint8Array }) | null): ProofSetup | null {\n if (!setup) {\n this.state.proofSetup = null;\n this.notify();\n return null;\n }\n let doc: DocumentColorProfile;\n try {\n doc = documentProfile(setup.profile);\n } catch (e) {\n throw new LayerOperationError(e instanceof Error ? e.message : String(e));\n }\n const intent = setup.intent ?? DEFAULT_PROOF.intent;\n if (!INTENTS.includes(intent)) throw new LayerOperationError(`Unknown rendering intent \"${String(intent)}\"; use ${INTENTS.join(', ')}.`);\n const next: ProofSetup = { ...DEFAULT_PROOF, ...setup, intent, profile: doc };\n if (!canBeDestination(resolveProfile(doc))) throw new LayerOperationError(`\"${doc.name}\" describes a source only, so colours cannot be proofed through it.`);\n this.state.proofSetup = next;\n this.notify();\n return next;\n }\n\n /**\n * Converts colours (`#rrggbb`) from one profile to another (default from the document's), with\n * `intent`. For RGB targets returns hex; for others the channel values 0..1 (CMYK, grey, Lab\n * encoded as L/100, (a+128)/255, (b+128)/255) and, for Lab, L*a*b*.\n */\n public convertColors(colors: string[], to: string | Uint8Array, options: { from?: string | Uint8Array; intent?: Intent; bpc?: boolean } = {}): Array<{ input: string; values: number[]; hex?: string; lab: [number, number, number] }> {\n let src: Profile;\n let dst: Profile;\n try {\n src = options.from !== undefined ? resolveProfile(documentProfile(options.from)) : resolveProfile(this.state.colorProfile);\n dst = resolveProfile(documentProfile(to));\n } catch (e) {\n throw new LayerOperationError(e instanceof Error ? e.message : String(e));\n }\n if (src.colorSpace !== 'rgb') throw new LayerOperationError('Colours are given as #rrggbb, so the source profile must be RGB.');\n const t = Transform.create(src, dst, { intent: options.intent ?? 'relative', bpc: options.bpc ?? true });\n const toLab = Transform.create(src, builtinProfile('lab-d50'), { intent: 'relative', bpc: false });\n return colors.map((input) => {\n const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${input}\"; use #rrggbb.`);\n const n = parseInt(m[1], 16);\n const rgb = [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n const out = new Float32Array(16);\n t.eval(rgb, out);\n const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 10000) / 10000);\n const l = new Float32Array(3);\n toLab.eval(rgb, l);\n const lab: [number, number, number] = [Math.round(l[0] * 10000) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];\n const res: { input: string; values: number[]; hex?: string; lab: [number, number, number] } = { input, values, lab };\n if (dst.colorSpace === 'rgb') res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, '0')).join('')}`;\n return res;\n });\n }\n\n\n /** The document's bits per channel (8 unless set to 16 or 32). */\n public get bitDepth(): BitDepth {\n return this.state.bitDepth ?? 8;\n }\n\n /**\n * Image › Mode › 8 / 16 / 32 Bits/Channel (one undo step). Going up costs nothing: layers keep\n * their pixels and read them at the new depth, and edits that understand depth keep the extra\n * precision from then on. Going down rounds every pixel layer's deep samples to the new depth\n * (8 bits: their 8-bit rendering; 32 → 16: colour above 1 is clipped). Returns the depth and\n * the ids of the layers whose pixels changed.\n */\n public setBitDepth(depth: BitDepth): { bitDepth: BitDepth; changedLayerIds: string[] } {\n if (!isBitDepth(depth)) throw new LayerOperationError(`Bit depth must be 8, 16 or 32, not ${String(depth)}.`);\n const changed: string[] = [];\n const convert = (layers: readonly ImageLayer[]): ImageLayer[] =>\n layers.map((l) => {\n if (l.type === 'group') {\n const children = convert(l.children);\n return children.some((c, i) => c !== l.children[i]) ? { ...l, children } : l;\n }\n if (l.type !== 'pixel') return l;\n const surface = convertSurfaceDepth(l.surface, depth);\n if (surface === l.surface) return l;\n changed.push(l.id);\n return { ...l, surface };\n });\n if (depth === this.bitDepth) return { bitDepth: depth, changedLayerIds: [] };\n const next = convert(this.state.layers);\n this.commitLayers(next, () => {\n if (depth === 8) delete this.state.bitDepth;\n else this.state.bitDepth = depth;\n });\n return { bitDepth: depth, changedLayerIds: changed };\n }\n\n /** How `toPsd` writes colour: RGB with the working space, or CMYK / Lab channels for those modes. */\n private psdColorOptions(warnings: string[]): { iccProfile?: Uint8Array; colorMode?: 'rgb' | 'cmyk' | 'lab'; colorPlanes?: (rgba: Uint8ClampedArray, n: number) => Uint8Array[] } {\n const m = this.state.colorMode;\n if (!m) return { iccProfile: profileBytes(this.state.colorProfile) };\n if (flattenLayers(this.state.layers).some((l) => l.type === 'adjustment' && l.visible)) {\n warnings.push(`Adjustment layers were written with their RGB settings; in a ${m.mode.toUpperCase()} file Photoshop applies them to its own channels, so they may look different there.`);\n }\n const planes = colorModePlanes(this.state.colorProfile, m);\n return m.mode === 'cmyk' ? { colorMode: 'cmyk', colorPlanes: planes, iccProfile: profileBytes(m.profile) } : { colorMode: 'lab', colorPlanes: planes };\n }\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 = this.checkPsdFonts(result);\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 if (result.bitDepth !== 8) s.bitDepth = result.bitDepth;\n else delete s.bitDepth;\n delete s.colorProfile;\n delete s.colorMode;\n if (result.sourceMode?.mode === 'cmyk') s.colorMode = { mode: 'cmyk', profile: result.sourceMode.profile, intent: 'relative' };\n else if (result.sourceMode?.mode === 'lab') s.colorMode = { mode: 'lab' };\n else if (result.iccProfile) {\n try {\n const doc = documentProfile(result.iccProfile);\n const cs = resolveProfile(doc).colorSpace;\n if (cs === 'rgb') {\n if (!isSrgb(doc)) s.colorProfile = doc;\n } else result.warnings.push(`The file's ${cs.toUpperCase()} profile \"${doc.name}\" was not used; its colours were read as sRGB.`);\n } catch (e) {\n result.warnings.push(`The embedded ICC profile could not be read (${e instanceof Error ? e.message : String(e)}); colours are read as sRGB.`);\n }\n }\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 * Type layers whose fonts the shaper lacks: they keep Photoshop's pixels, are marked with their\n * `missingFonts`, and a warning names them (an edit draws them with a fallback).\n */\n private checkPsdFonts(result: PsdImportResult): ImageLayer[] {\n const shaper = this.getTextShaper();\n const types = flattenLayers(result.layers).filter((l): l is TypeLayer => l.type === 'type');\n if (!types.length) return result.layers;\n let layers = result.layers;\n const missing = new Map<string, string[]>();\n for (const l of types) {\n const fonts = shaper ? layoutText(l, shaper).missingFonts : [...new Set(l.runs.map((r) => r.style.fontFamily))];\n if (!fonts.length) continue;\n layers = mapLayer(layers, l.id, () => ({ ...l, missingFonts: fonts }));\n for (const f of fonts) missing.set(f, [...(missing.get(f) ?? []), l.name]);\n }\n for (const [font, names] of missing) {\n result.warnings.push(\n shaper\n ? `The font \"${font}\" is missing here: the type shows Photoshop's pixels until it is edited, then a fallback font (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`\n : `Type layers show Photoshop's pixels; nothing here can draw text, so they cannot be edited (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`\n );\n if (!shaper) break;\n }\n return layers;\n }\n\n /**\n * The document as an RGB PSD at its bit depth (8, 16 or 32) (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 === 'shape' || layer.type === 'type') {\n // Written as Photoshop shape and type layers (with their pixels for viewers that need them).\n if (layer.type === 'type' && layer.missingFonts?.length) add(`Type layers drawn with a fallback for missing fonts (${layer.missingFonts.join(', ')}) keep the font names, so Photoshop may draw them differently`, layer.name);\n const out = { ...layer };\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n }\n if (layer.type === 'smart') {\n const out = { ...layer };\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\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 color = this.psdColorOptions(warnings);\n // CMYK and Lab files are written at 8 bits.\n if (color.colorMode && this.bitDepth !== 8) warnings.push(`${color.colorMode.toUpperCase()} files are written at 8 bits per channel; the ${this.bitDepth}-bit samples were rounded.`);\n const depth = color.colorMode ? 8 : this.bitDepth;\n // At 16 or 32 bits the merged image is composited at that depth too.\n const mergedDeep = depth === 8 ? undefined : workingToSamples(compositeDeep(prepared, canvas, sources, depth), depth);\n const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, ...color });\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 const env: ShapeEnv = { ...next.dimensions, pattern: (pid) => this.state.patterns?.find((p) => p.id === pid) ?? null };\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 } else if (l.type === 'shape') {\n const a = l.annotation;\n const scaled = a ? scaleAnnotation(a, sx, sy) : null;\n const fromAnnotation = scaled ? shapeFromAnnotation(scaled) : null;\n const m: AffineMatrix = [sx, 0, 0, sy, 0, 0];\n const components = fromAnnotation?.components ?? transformShapeComponents(l.components, (p) => ({ x: p.x * sx, y: p.y * sy }), m);\n const moved: ShapeLayer = { ...l, components, ...(fromAnnotation ? { fill: fromAnnotation.fill, stroke: fromAnnotation.stroke } : {}) };\n if (scaled && fromAnnotation) moved.annotation = scaled;\n else delete moved.annotation;\n layers = mapLayer(layers, l.id, () => rerenderShape(moved, env));\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' || ((l.type === 'shape' || l.type === 'type') && l.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 const storedPatterns = deserializePatterns(data.patterns, []);\n s.layers = deserializeLayers(data.layers, {\n assets: data.assets ?? {},\n problems,\n smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],\n canvas,\n pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null,\n text: this.textEnv(),\n });\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 (data.bitDepth === 16 || data.bitDepth === 32) s.bitDepth = data.bitDepth;\n else delete s.bitDepth;\n const profile = data.colorProfile ? deserializeColorProfile(data.colorProfile, problems) : null;\n if (profile) s.colorProfile = profile;\n else delete s.colorProfile;\n const cmode = data.colorMode ? deserializeColorMode(data.colorMode, problems) : null;\n if (cmode) s.colorMode = cmode;\n else delete s.colorMode;\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 delete s.bitDepth;\n delete s.colorProfile;\n delete s.colorMode;\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 and a bit depth are only stored in the layered schema.\n if (view.exact && !this.state.globalLight && !this.state.patterns?.length && this.bitDepth === 8 && !this.state.colorProfile && !this.state.colorMode) {\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 if (this.state.bitDepth === 16 || this.state.bitDepth === 32) out.bitDepth = this.state.bitDepth;\n if (this.state.colorProfile) out.colorProfile = serializeColorProfile(this.state.colorProfile);\n if (this.state.colorMode) out.colorMode = serializeColorMode(this.state.colorMode);\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 === 'shape' || l.type === 'type') collectTiles(l.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 with every layer above `id` left out (Sample: Current & Below). */\nfunction layersThrough(layers: readonly ImageLayer[], id: string): ImageLayer[] {\n const walk = (list: readonly ImageLayer[]): { out: ImageLayer[]; found: boolean } => {\n const out: ImageLayer[] = [];\n for (const l of list) {\n if (l.id === id) {\n out.push(l);\n return { out, found: true };\n }\n if (l.type === 'group') {\n const sub = walk(l.children);\n if (sub.found) {\n out.push({ ...l, children: sub.out });\n return { out, found: true };\n }\n }\n out.push(l);\n }\n return { out, found: false };\n };\n return walk(layers).out;\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' || ((l.type === 'shape' || l.type === 'type') && l.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', 'shape', 'type']);\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\nexport interface ImageFilterPreset {\n id: string;\n label: string;\n adjustments: Partial<ImageAdjustments>;\n}\n\n/** Built-in colour-grade presets for quick one-click adjustment looks. */\nexport const IMAGE_FILTER_PRESETS: readonly ImageFilterPreset[] = [\n {\n id: 'none',\n label: 'Original',\n adjustments: {\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 },\n },\n {\n id: 'dramatic',\n label: 'Dramatic',\n adjustments: {\n brightness: -5,\n contrast: 45,\n saturation: 20,\n exposure: 5,\n temperature: -10,\n tint: 0,\n },\n },\n {\n id: 'cyberpunk',\n label: 'Cyberpunk',\n adjustments: {\n brightness: 5,\n contrast: 35,\n saturation: 55,\n exposure: 0,\n temperature: -25,\n tint: 40,\n },\n },\n {\n id: 'vintage',\n label: 'Vintage',\n adjustments: {\n brightness: 8,\n contrast: -15,\n saturation: -35,\n exposure: 5,\n temperature: 35,\n tint: 10,\n },\n },\n {\n id: 'warm',\n label: 'Warm',\n adjustments: {\n brightness: 5,\n contrast: 10,\n saturation: 15,\n exposure: 0,\n temperature: 45,\n tint: 5,\n },\n },\n {\n id: 'cool',\n label: 'Cool',\n adjustments: {\n brightness: 0,\n contrast: 15,\n saturation: 10,\n exposure: 0,\n temperature: -45,\n tint: -10,\n },\n },\n {\n id: 'grayscale',\n label: 'B&W',\n adjustments: {\n brightness: 0,\n contrast: 20,\n saturation: -100,\n exposure: 0,\n temperature: 0,\n tint: 0,\n },\n },\n {\n id: 'noir',\n label: 'Noir',\n adjustments: {\n brightness: -15,\n contrast: 65,\n saturation: -100,\n exposure: -5,\n temperature: 0,\n tint: 0,\n },\n },\n {\n id: 'fade',\n label: 'Fade',\n adjustments: {\n brightness: 15,\n contrast: -25,\n saturation: -20,\n exposure: 10,\n temperature: 10,\n tint: 0,\n },\n },\n];\n\nexport interface ImageCropPreset {\n id: string;\n label: string;\n aspectRatio: number | null;\n}\n\n/** Standard aspect-ratio presets for cropping. */\nexport const IMAGE_CROP_PRESETS: readonly ImageCropPreset[] = [\n { id: 'free', label: 'Free', aspectRatio: null },\n { id: '1:1', label: '1:1', aspectRatio: 1 },\n { id: '16:9', label: '16:9', aspectRatio: 16 / 9 },\n { id: '4:3', label: '4:3', aspectRatio: 4 / 3 },\n { id: '4:5', label: '4:5', aspectRatio: 4 / 5 },\n { id: '9:16', label: '9:16', aspectRatio: 9 / 16 },\n];\n\n/** Numeric controls for depth-mask near/far threshold range. */\nexport const DEPTH_MASK_CONTROLS: readonly ControlSpec[] = [\n {\n id: 'near',\n label: 'Near Threshold',\n section: 'effects',\n min: 0,\n max: 255,\n step: 1,\n default: 0,\n decimals: 0,\n track: plain,\n },\n {\n id: 'far',\n label: 'Far Threshold',\n section: 'effects',\n min: 0,\n max: 255,\n step: 1,\n default: 255,\n decimals: 0,\n track: plain,\n },\n];\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 * `set_bit_depth`, `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 bits per channel (bitDepth 8, 16 or 32), 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(), bitDepth: controller.bitDepth, layers: controller.getAdjustmentLayers().length, ...history() })\n };\n\n const setBitDepth: AgentTool = {\n name: 'set_bit_depth',\n title: 'Set the bit depth',\n description:\n \"Image › Mode › 8 / 16 / 32 Bits/Channel. 16-bit keeps smooth gradients and survives strong adjustments without banding; 32-bit holds linear-light float values (HDR, colour above 1). Going up costs nothing and changes no pixels; going down rounds every pixel layer's extra precision away (32 → 16 also clips colour above 1). The viewport always shows an 8-bit rendering. One undo step. Returns the bitDepth and changedLayerIds (the layers whose samples were rounded).\",\n inputSchema: {\n type: 'object',\n properties: { depth: { type: 'number', enum: [8, 16, 32], description: 'Bits per channel: 8, 16 or 32 (float, linear light).' } },\n required: ['depth'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the bit depth', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },\n handler: (args) => ({ ...controller.setBitDepth(args.depth as 8 | 16 | 32), ...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, setBitDepth, 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 'shape':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.shape = {\n components: layer.components.map((c) => ({ op: c.op, kind: c.live?.kind ?? 'path' })),\n fill: layer.fill ? layer.fill.type : null,\n stroke: layer.stroke ? { width: layer.stroke.width, paint: layer.stroke.paint.type } : null,\n };\n if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'type':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.text = {\n text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}…` : layer.text,\n frame: layer.frame.kind,\n runs: layer.runs.length,\n rendering: layer.rendering === 'stored' ? 'stored' : 'live',\n missingFonts: layer.missingFonts ?? [],\n };\n if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\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, shape, type, smart, annotation), name, document bounds (pixel, shape, type and smart 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), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), a text summary (the text, point or box frame, run count, live or stored pixels, missing fonts; details from get_text_layer), 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 a shape or type layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, 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, shape, smart object, 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, shape, type, smart object, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.\",\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, S, J, Shift+J, O, Shift+O, [ ], Shift+[ ],\n * number keys, D, X) and a pointer gesture that turns pointer events into strokes (Brush, Mixer\n * Brush, Eraser and the retouching tools; Alt-click sets the clone source) or layer moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { shapeGeometryBounds } from './shape';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type MixerSettings, type RetouchSettings } 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 { id: 'clone', label: 'Clone', shortcut: 'S' },\n { id: 'heal', label: 'Heal', shortcut: 'Shift+J' },\n { id: 'spot-heal', label: 'Spot Heal', shortcut: 'J' },\n { id: 'smudge', label: 'Smudge', shortcut: '' },\n { id: 'dodge', label: 'Dodge', shortcut: 'O' },\n { id: 'burn', label: 'Burn', shortcut: 'Shift+O' },\n { id: 'sponge', label: 'Sponge', shortcut: 'Shift+O' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => PAINT_TOOLS.some((t) => t.id === tool);\n\n/** The paint tools that retouch (their options show instead of the brush colours). */\nexport const isRetouchPaintTool = (tool: string): boolean =>\n tool === 'clone' || tool === 'heal' || tool === 'spot-heal' || tool === 'smudge' || tool === 'dodge' || tool === 'burn' || tool === 'sponge';\n\n/** Retouch sliders: dodge / burn exposure and smudge strength (shown in %). */\nexport const RETOUCH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'exposure' | 'strength'; tools: readonly PaintTool[] })[] = [\n { id: 'exposure', label: 'Exposure', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%', tools: ['dodge', 'burn'] },\n { id: 'strength', label: 'Strength', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%', tools: ['smudge'] },\n];\n\n/** Retouch choices, each a row of toggle buttons, with the tools that show it. */\nexport const RETOUCH_CHOICES: readonly {\n id: 'sample' | 'spotType' | 'range' | 'spongeMode';\n label: string;\n tools: readonly PaintTool[];\n options: readonly { id: string; label: string; title: string }[];\n}[] = [\n {\n id: 'sample',\n label: 'Sample',\n tools: ['clone', 'heal', 'spot-heal'],\n options: [\n { id: 'current', label: 'Current', title: 'Sample the layer being retouched' },\n { id: 'current-below', label: 'Current & Below', title: 'Sample this layer and the layers under it' },\n { id: 'all', label: 'All', title: 'Sample every layer (retouch onto an empty layer)' },\n ],\n },\n {\n id: 'spotType',\n label: 'Type',\n tools: ['spot-heal'],\n options: [\n { id: 'proximity-match', label: 'Proximity Match', title: 'Copy the best matching patch nearby' },\n { id: 'smooth', label: 'Smooth', title: 'Fill smoothly from the edges' },\n ],\n },\n {\n id: 'range',\n label: 'Range',\n tools: ['dodge', 'burn'],\n options: [\n { id: 'shadows', label: 'Shadows', title: 'Change the dark tones most' },\n { id: 'midtones', label: 'Midtones', title: 'Change the middle tones most' },\n { id: 'highlights', label: 'Highlights', title: 'Change the light tones most' },\n ],\n },\n {\n id: 'spongeMode',\n label: 'Mode',\n tools: ['sponge'],\n options: [\n { id: 'desaturate', label: 'Desaturate', title: 'Take colour out' },\n { id: 'saturate', label: 'Saturate', title: 'Add colour' },\n ],\n },\n];\n\n/** Retouch on / off options, with the tools that show each. */\nexport const RETOUCH_TOGGLES: readonly { id: 'aligned' | 'protectTones' | 'vibrance'; label: string; tools: readonly PaintTool[] }[] = [\n { id: 'aligned', label: 'Aligned', tools: ['clone', 'heal'] },\n { id: 'protectTones', label: 'Protect tones', tools: ['dodge', 'burn'] },\n { id: 'vibrance', label: 'Vibrance', tools: ['sponge'] },\n];\n\n/** Sets one retouch choice from the options bar. */\nexport function setRetouchChoice(controller: ImageEditorController, id: (typeof RETOUCH_CHOICES)[number]['id'], value: string): void {\n controller.setPaintOptions({ retouch: { [id]: value } as Partial<RetouchSettings> });\n}\n\n/** What the options bar says about the clone source. */\nexport function cloneSourceLabel(controller: ImageEditorController): string {\n const cs = controller.getState().paint.cloneSource;\n return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ''}` : 'Alt-click to set the source';\n}\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 's':\n controller.setTool('clone');\n return true;\n case 'j':\n controller.setTool(e.shiftKey || key === 'J' ? (controller.getState().activeTool === 'heal' ? 'spot-heal' : 'heal') : 'spot-heal');\n return true;\n case 'o': {\n // O: Dodge; Shift+O cycles Dodge → Burn → Sponge.\n const cur = controller.getState().activeTool;\n const next = e.shiftKey || key === 'O' ? (cur === 'dodge' ? 'burn' : cur === 'burn' ? 'sponge' : 'dodge') : 'dodge';\n controller.setTool(next);\n return true;\n }\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 /** Alt (Option) held on the press: with the Clone Stamp or Healing Brush it sets the source. */\n altKey?: boolean;\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' && layer.type !== 'shape' && layer.type !== 'smart') throw new Error(`Only pixel, shape and smart object 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 // Where `offsetLayer` measures the position: the offset, a shape's top-left, a smart object's first corner.\n const origin = layer.type === 'pixel' ? { ...layer.offset } : layer.type === 'smart' ? { ...layer.placement.corners[0] } : (() => {\n const b = shapeGeometryBounds(layer);\n return b ? { x: b.x, y: b.y } : { x: 0, y: 0 };\n })();\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };\n return null;\n }\n if ((tool === 'clone' || tool === 'heal') && sample.altKey) {\n this.controller.setCloneSource({ x: sample.x, y: sample.y });\n return null;\n }\n if (isPaintTool(tool)) {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([{ x: sample.x, y: sample.y, pressure: sample.pressure }]);\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 { BUILTIN_PROFILES, INTENTS, type Intent } from '../cms';\nimport { base64ToBytes } from '../raster/png';\nimport { LayerOperationError } from './layers/ops';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nconst PROFILE = {\n type: 'string',\n description: `A built-in profile id (${BUILTIN_PROFILES.map((b) => b.id).join(', ')}; names like \"Adobe RGB (1998)\" work too), or \"icc:<base64>\" for ICC profile bytes.`,\n} as const;\n\nconst INTENT = {\n type: 'string',\n enum: [...INTENTS],\n description:\n 'Rendering intent (default relative): relative (colorimetric, white to white, clips out-of-gamut colours: best for most photos), perceptual (compresses the whole gamut smoothly), saturation (keeps colours vivid, for graphics), absolute (keeps the source white too: for proofing a paper colour).',\n} as const;\n\nfunction profileArg(v: unknown): string | Uint8Array {\n const s = String(v);\n if (s.startsWith('icc:')) {\n try {\n return base64ToBytes(s.slice(4));\n } catch {\n throw new LayerOperationError('The icc: value is not valid base64.');\n }\n }\n return s;\n}\n\n/**\n * Agent tools for colour management: `get_color_profile` (the document's profile, mode, Proof\n * Setup and the built-in profiles), `assign_profile`, `convert_to_profile`, `set_color_mode`\n * (each one undo step), `set_proof_setup` (view state) and `convert_colors` (read-only).\n */\nexport function createImageColorTools(controller: ImageEditorController): AgentTool[] {\n const getProfile: AgentTool = {\n name: 'get_color_profile',\n title: 'Get the colour profile',\n description:\n \"Reads the document's colour profile (its working space: name, builtin id or null for an embedded profile, colorSpace, srgb true when it means sRGB, mode rgb / cmyk / lab and, for cmyk, cmykProfile), the Proof Setup (profile, intent, bpc, simulatePaper, gamutWarning, or null) and the built-in profiles you can assign, convert to or proof with ({id, description, colorSpace}).\",\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get the colour profile', ...READ },\n handler: () => {\n const proof = controller.getState().proofSetup;\n return {\n profile: controller.getColorProfile(),\n proof: proof ? { profile: proof.profile.name, builtin: proof.profile.builtin ?? null, intent: proof.intent, bpc: proof.bpc, simulatePaper: proof.simulatePaper, gamutWarning: proof.gamutWarning } : null,\n builtins: BUILTIN_PROFILES.map((b) => ({ ...b })),\n };\n },\n };\n\n const assign: AgentTool = {\n name: 'assign_profile',\n title: 'Assign a colour profile',\n description:\n \"Edit › Assign Profile: the document's RGB numbers now mean colours in `profile`; no pixel changes, so the image looks different unless the profiles match. Use it when a file was saved without its profile, or with the wrong one. To keep the look and change the numbers, use convert_to_profile. RGB profiles only. One undo step. Returns the new profile.\",\n inputSchema: { type: 'object', properties: { profile: PROFILE }, required: ['profile'], additionalProperties: false },\n annotations: { title: 'Assign a colour profile', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: (args) => controller.assignProfile(profileArg(args.profile)),\n };\n\n const convert: AgentTool = {\n name: 'convert_to_profile',\n title: 'Convert to a colour profile',\n description:\n \"Edit › Convert to Profile: converts every pixel layer's numbers so its colours stay the same in `profile`, then makes `profile` the document's. Colours outside the new space are mapped by `intent`. Shape and type layers, layer effects and smart objects keep their numbers and are listed in unconvertedLayerIds. RGB profiles only (proof CMYK with set_proof_setup). One undo step. Returns the profile, convertedLayerIds and unconvertedLayerIds.\",\n inputSchema: {\n type: 'object',\n properties: {\n profile: PROFILE,\n intent: INTENT,\n bpc: { type: 'boolean', description: 'Black point compensation: map black to black (default true).' },\n },\n required: ['profile'],\n additionalProperties: false,\n },\n annotations: { title: 'Convert to a colour profile', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n handler: (args) => controller.convertToProfile(profileArg(args.profile), { intent: args.intent as Intent | undefined, bpc: args.bpc as boolean | undefined }),\n };\n\n const proof: AgentTool = {\n name: 'set_proof_setup',\n title: 'Set up soft proofing',\n description:\n \"View › Proof Setup: shows the document in the viewport as `profile` would reproduce it (for example coated-cmyk for a press), with `intent`; `simulatePaper` shows the paper white and ink black; `gamutWarning` greys out colours the profile cannot reproduce. Pass off true to stop proofing. View state, not an undo step; pixels never change. Check the result with capture_view. Returns the setup (or null).\",\n inputSchema: {\n type: 'object',\n properties: {\n profile: PROFILE,\n intent: INTENT,\n bpc: { type: 'boolean', description: 'Black point compensation (default true).' },\n simulatePaper: { type: 'boolean', description: 'Simulate paper colour and black ink (default false).' },\n gamutWarning: { type: 'boolean', description: 'Grey out out-of-gamut colours (default false).' },\n off: { type: 'boolean', description: 'Turn proofing off instead (default false).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set up soft proofing', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: (args) => {\n if (args.off === true) {\n if (args.profile !== undefined) throw new LayerOperationError('Give either off true or a profile, not both.');\n return controller.setProofSetup(null);\n }\n if (args.profile === undefined) throw new LayerOperationError('Pass the profile to proof with (e.g. coated-cmyk), or off true.');\n const s = controller.setProofSetup({\n profile: profileArg(args.profile),\n ...(args.intent !== undefined ? { intent: args.intent as Intent } : {}),\n ...(args.bpc !== undefined ? { bpc: args.bpc as boolean } : {}),\n ...(args.simulatePaper !== undefined ? { simulatePaper: args.simulatePaper as boolean } : {}),\n ...(args.gamutWarning !== undefined ? { gamutWarning: args.gamutWarning as boolean } : {}),\n })!;\n return { profile: s.profile.name, builtin: s.profile.builtin ?? null, intent: s.intent, bpc: s.bpc, simulatePaper: s.simulatePaper, gamutWarning: s.gamutWarning };\n },\n };\n\n const convertColors: AgentTool = {\n name: 'convert_colors',\n title: 'Convert colours between profiles',\n description:\n \"Converts 1 to 64 #rrggbb colours from the document's profile (or `from`) into `to` with `intent`: returns per colour the channel values 0..1 (RGB, CMYK, grey; Lab as L/100, (a+128)/255, (b+128)/255), a hex for RGB targets, and L*a*b* (D50). Use it to answer \\\"what are these CMYK values\\\" or to pick colours that survive printing. Changes nothing.\",\n inputSchema: {\n type: 'object',\n properties: {\n colors: { type: 'array', minItems: 1, maxItems: 64, items: { type: 'string', description: 'A colour #rrggbb.' }, description: 'Colours in the source profile.' },\n to: PROFILE,\n from: { ...PROFILE, description: `Source profile (default the document's). ${PROFILE.description}` },\n intent: INTENT,\n bpc: { type: 'boolean', description: 'Black point compensation (default true).' },\n },\n required: ['colors', 'to'],\n additionalProperties: false,\n },\n annotations: { title: 'Convert colours between profiles', ...READ },\n handler: (args) => ({\n colors: controller.convertColors(args.colors as string[], profileArg(args.to), {\n ...(args.from !== undefined ? { from: profileArg(args.from) } : {}),\n intent: args.intent as Intent | undefined,\n bpc: args.bpc as boolean | undefined,\n }),\n }),\n };\n\n const setMode: AgentTool = {\n name: 'set_color_mode',\n title: 'Set the colour mode',\n description:\n \"Image › Mode: rgb, cmyk (for print: give the press `profile`, default coated-cmyk, and the `intent` RGB is separated with) or lab. The document is still edited as RGB in its working space; a CMYK document is soft-proofed through its profile in the viewport (unless set_proof_setup says otherwise), and export_psd writes CMYK or Lab channels with the profile. Changes no pixels. One undo step. Returns the profile summary with mode and cmykProfile.\",\n inputSchema: {\n type: 'object',\n properties: {\n mode: { type: 'string', enum: ['rgb', 'cmyk', 'lab'], description: 'The colour mode.' },\n profile: { ...PROFILE, description: `CMYK mode only: the CMYK profile (default coated-cmyk). ${PROFILE.description}` },\n intent: INTENT,\n },\n required: ['mode'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the colour mode', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: (args) => {\n if (args.mode !== 'cmyk' && (args.profile !== undefined || args.intent !== undefined)) throw new LayerOperationError('profile and intent apply to CMYK mode only.');\n return controller.setColorMode(args.mode as 'rgb' | 'cmyk' | 'lab', {\n ...(args.profile !== undefined ? { profile: profileArg(args.profile) } : {}),\n ...(args.intent !== undefined ? { intent: args.intent as Intent } : {}),\n });\n },\n };\n\n return [getProfile, assign, convert, proof, convertColors, setMode];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController, RetouchTool } from './controller';\nimport { MAX_BRUSH_SIZE, RETOUCH_SAMPLES, SPOT_HEAL_TYPES, TONE_RANGES, type BrushSettings, type RetouchSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\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;\n\nconst POINTS = {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The stroke path in document pixels; one point is 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} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush tip; 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). Retouching usually wants a soft tip (0..0.5).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity (Clone Stamp and Healing Brush only; default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab (default 1). For the Sponge, how strongly each pass moves saturation.' },\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 },\n additionalProperties: false,\n} as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to retouch (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 filter mask. Masks are grey values and sample only themselves.\",\n },\n} as const;\n\nconst SAMPLE = {\n type: 'string',\n enum: [...RETOUCH_SAMPLES],\n description:\n 'What is sampled: current (default: the target layer), current-below (the composite of this layer and everything under it), or all (the composite of every layer). Sample current-below or all to retouch onto an empty layer above the image.',\n} as const;\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\nfunction checkSource(controller: ImageEditorController, source: unknown): { x: number; y: number } | undefined {\n if (source === undefined) return undefined;\n const p = source as { x: number; y: number };\n const { width, height } = controller.getState().imageDimensions;\n if (!(p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {\n throw new LayerOperationError(`source (${p.x}, ${p.y}) is outside the ${width} × ${height} canvas; pick a point on the image to sample.`);\n }\n return p;\n}\n\nconst SOURCE = {\n type: 'object',\n description: 'Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).',\n properties: { x: { type: 'number', description: 'Source x in px.' }, y: { type: 'number', description: 'Source y in px.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\n\n/**\n * Agent tools for the retouching tools, one per tool: `clone_stamp`, `heal_brush`, `spot_heal`,\n * `smudge`, `dodge`, `burn` and `sponge` (each one stroke, one undo step, returning the layer,\n * channel, changed bounds and dab count), plus the view state they share: `get_retouch_options`,\n * `set_retouch_options` and `set_clone_source`.\n */\nexport function createImageRetouchTools(controller: ImageEditorController): AgentTool[] {\n const stroke = (tool: RetouchTool, args: Record<string, unknown>, retouch: Partial<RetouchSettings>) =>\n controller.paintStroke({\n tool,\n points: args.points as StrokePoint[],\n brush: args.brush as Partial<BrushSettings> | undefined,\n retouch,\n source: checkSource(controller, args.source),\n ...readTarget(args),\n });\n\n const cloneStamp: AgentTool = {\n name: 'clone_stamp',\n title: 'Clone Stamp stroke',\n description:\n \"Copies pixels from the clone source along a stroke (Photoshop's Clone Stamp): each point paints what lies at point + offset, where the offset is source − the stroke's first point. With aligned on, later strokes keep that offset; off, every stroke starts again at the source point. Samples the layer, the layers up to it, or all layers, from before the stroke. The selection limits it; a transparency-locked layer keeps its alpha. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n source: SOURCE,\n aligned: { type: 'boolean', description: 'Keep the offset for the next strokes (default the current option, true).' },\n sample: SAMPLE,\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Clone Stamp stroke', ...WRITE },\n handler: (args) => stroke('clone', args, pick(args, ['aligned', 'sample'])),\n };\n\n const healBrush: AgentTool = {\n name: 'heal_brush',\n title: 'Healing Brush stroke',\n description:\n \"Heals along a stroke from the clone source (Photoshop's Healing Brush): the source's texture is cloned, then blended so its colour and light match the pixels around the stroke (Poisson seamless cloning, applied when the stroke ends). Use it for blemishes and dust where the area around them is the right tone; use clone_stamp to copy exactly. Same source, offset and aligned rules as clone_stamp. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n source: SOURCE,\n aligned: { type: 'boolean', description: 'Keep the offset for the next strokes (default the current option, true).' },\n sample: SAMPLE,\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Healing Brush stroke', ...WRITE },\n handler: (args) => stroke('heal', args, pick(args, ['aligned', 'sample'])),\n };\n\n const spotHeal: AgentTool = {\n name: 'spot_heal',\n title: 'Spot Healing stroke',\n description:\n \"Removes what a stroke covers without a source point (Photoshop's Spot Healing Brush): proximity-match copies the best matching patch nearby and blends it in; smooth fills from the edges inwards (for spots on smooth areas). Paint just over the spot with a brush a little larger than it. One undo step. Returns the layerId, channel, changed bounds, dab count and spotHeal {offset} (the patch offset copied, or null for a smooth fill).\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n type: { type: 'string', enum: [...SPOT_HEAL_TYPES], description: 'proximity-match (default) or smooth.' },\n sample: SAMPLE,\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Spot Healing stroke', ...WRITE },\n handler: (args) => stroke('spot-heal', args, { ...pick(args, ['sample']), ...(args.type !== undefined ? { spotType: args.type as RetouchSettings['spotType'] } : {}) }),\n };\n\n const smudge: AgentTool = {\n name: 'smudge',\n title: 'Smudge stroke',\n description:\n \"Drags pixels along a stroke like a finger through wet paint (Photoshop's Smudge): each dab lays what the finger carries and picks up what was under it, so colour is pulled in the stroke's direction and fades with distance. One undo step. Returns the layerId, channel, changed bounds and dab count.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'How far colour is dragged, 0..1 (default the current option, 0.5).' },\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Smudge stroke', ...WRITE },\n handler: (args) => stroke('smudge', args, pick(args, ['strength'])),\n };\n\n const toneSchema = (verb: string) => ({\n type: 'object' as const,\n properties: {\n points: POINTS,\n range: { type: 'string', enum: [...TONE_RANGES], description: `Which tones to ${verb} most: shadows, midtones (default the current option, midtones) or highlights.` },\n exposure: { type: 'number', minimum: 0, maximum: 1, description: 'Strength 0..1 (default the current option, 0.5). Overlapping dabs in one stroke never go past it; another stroke builds on it.' },\n protectTones: { type: 'boolean', description: 'Change luma only, keeping hue and saturation (default true).' },\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n }) as const;\n\n const dodge: AgentTool = {\n name: 'dodge',\n title: 'Dodge stroke',\n description:\n \"Lightens the pixels a stroke covers (Photoshop's Dodge), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count. Inspect the result with capture_view or get_histogram; several light strokes look more natural than one strong one.\",\n inputSchema: toneSchema('lighten'),\n annotations: { title: 'Dodge stroke', ...WRITE },\n handler: (args) => stroke('dodge', args, pick(args, ['range', 'exposure', 'protectTones'])),\n };\n\n const burn: AgentTool = {\n name: 'burn',\n title: 'Burn stroke',\n description:\n \"Darkens the pixels a stroke covers (Photoshop's Burn), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count.\",\n inputSchema: toneSchema('darken'),\n annotations: { title: 'Burn stroke', ...WRITE },\n handler: (args) => stroke('burn', args, pick(args, ['range', 'exposure', 'protectTones'])),\n };\n\n const spongeTool: AgentTool = {\n name: 'sponge',\n title: 'Sponge stroke',\n description:\n \"Saturates or desaturates the pixels a stroke covers (Photoshop's Sponge); passes compound, and brush.flow sets how much each moves. Pixels only (a mask has no colour). One undo step. Returns the layerId, channel, changed bounds and dab count.\",\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n mode: { type: 'string', enum: ['desaturate', 'saturate'], description: 'desaturate (default the current option, desaturate) or saturate.' },\n vibrance: { type: 'boolean', description: 'Damp the change where colours would clip or collapse to grey (default true).' },\n brush: BRUSH,\n layerId: TARGET.layerId,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Sponge stroke', ...WRITE },\n handler: (args) =>\n stroke('sponge', { ...args, channel: 'pixels' }, { ...pick(args, ['vibrance']), ...(args.mode !== undefined ? { spongeMode: args.mode as RetouchSettings['spongeMode'] } : {}) }),\n };\n\n const getOptions: AgentTool = {\n name: 'get_retouch_options',\n title: 'Get retouch options',\n description:\n 'Reads the retouching tools\\' options (aligned, sample, spotType, range, exposure, protectTones, spongeMode, vibrance, strength) and the clone source ({x, y, offset} or null; offset is the aligned offset the last clone or heal stroke fixed). Call it before clone_stamp or heal_brush without a source to see where they will sample.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get retouch options', ...READ },\n handler: () => {\n const { retouch, cloneSource } = controller.getState().paint;\n return { options: { ...retouch }, cloneSource: cloneSource ? { ...cloneSource } : null };\n },\n };\n\n const setOptions: AgentTool = {\n name: 'set_retouch_options',\n title: 'Set retouch options',\n description:\n 'Sets the retouching tools\\' defaults (view state, not an undo step); each stroke tool can also override them per call. Changing aligned clears the aligned offset. Returns the options after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n aligned: { type: 'boolean', description: 'Clone / heal: keep the offset from stroke to stroke (default true).' },\n sample: SAMPLE,\n spotType: { type: 'string', enum: [...SPOT_HEAL_TYPES], description: 'Spot heal: proximity-match (default) or smooth.' },\n range: { type: 'string', enum: [...TONE_RANGES], description: 'Dodge / burn: shadows, midtones (default) or highlights.' },\n exposure: { type: 'number', minimum: 0, maximum: 1, description: 'Dodge / burn strength 0..1 (default 0.5).' },\n protectTones: { type: 'boolean', description: 'Dodge / burn: keep hue and saturation (default true).' },\n spongeMode: { type: 'string', enum: ['desaturate', 'saturate'], description: 'Sponge: desaturate (default) or saturate.' },\n vibrance: { type: 'boolean', description: 'Sponge: damp clipping (default true).' },\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'Smudge strength 0..1 (default 0.5).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set retouch options', ...WRITE, idempotentHint: true },\n handler: (args) => ({ ...controller.setPaintOptions({ retouch: args as Partial<RetouchSettings> }).retouch }),\n };\n\n const setSource: AgentTool = {\n name: 'set_clone_source',\n title: 'Set the clone source',\n description:\n \"Sets the Clone Stamp and Healing Brush source point (Photoshop's Alt-click), in document pixels, or clears it with clear true. The next clone_stamp or heal_brush stroke fixes the offset from its first point. View state, not an undo step. Returns the clone source {x, y, offset} (or null).\",\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Source x in px (required unless clear).' },\n y: { type: 'number', description: 'Source y in px (required unless clear).' },\n clear: { type: 'boolean', description: 'Remove the source point instead (default false).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the clone source', ...WRITE, idempotentHint: true },\n handler: (args) => {\n if (args.clear === true) {\n if (args.x !== undefined || args.y !== undefined) throw new LayerOperationError('Give either clear true or x and y, not both.');\n return controller.setCloneSource(null);\n }\n if (typeof args.x !== 'number' || typeof args.y !== 'number') throw new LayerOperationError('Pass x and y (document pixels) for the source point, or clear true to remove it.');\n return controller.setCloneSource(checkSource(controller, { x: args.x, y: args.y })!);\n },\n };\n\n return [cloneStamp, healBrush, spotHeal, smudge, dodge, burn, spongeTool, getOptions, setOptions, setSource];\n}\n\nfunction pick(args: Record<string, unknown>, keys: string[]): Partial<RetouchSettings> {\n const out: Record<string, unknown> = {};\n for (const k of keys) if (args[k] !== undefined) out[k] = args[k];\n return out as Partial<RetouchSettings>;\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; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); shape layers come in as live shapes (paths, path operations, fill, stroke, live rectangles, ellipses and lines); type layers come in as live text (runs, paragraphs, frame, transform) showing Photoshop’s pixels until edited, with a warning for fonts missing here; fill layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).',\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 RGB PSD at its bit depth (8, 16 or 32; see set_bit_depth; a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Shape layers are written as Photoshop shape layers (path, fill, stroke, live shapes) and type layers as Photoshop type layers (text, runs, paragraphs, frame; blocks opened from a PSD are written back unchanged while the layer is). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor\\'s composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.',\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 } from '../tools/registry.js';\nimport type { ImageEditorController, ShapeGeometryInput, ShapeLayerInit } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError } from './layers/ops';\nimport { requireShapeLayer } from './layers/shape';\nimport { describeLayer } 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 LAYER_ID = { type: 'string', description: 'Shape layer id from list_layers (type shape) or add_shape_layer.' } as const;\nconst COMPONENT_INDEX = { type: 'integer', minimum: 0, description: 'Component index from get_shape_layer (0 = bottom component).' } as const;\nconst OP = {\n type: 'string',\n enum: ['combine', 'subtract', 'intersect', 'exclude'],\n description: 'How the component combines with the components below it (Photoshop path operations; default combine). The bottom component always combines.',\n} as const;\n\nconst NUM = (description: string) => ({ type: 'number', description }) as const;\nconst MATRIX = {\n type: 'array',\n items: { type: 'number', description: 'Matrix entry.' },\n minItems: 6,\n maxItems: 6,\n description: \"Optional affine map from the shape's own frame to the document [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f), e.g. a rotation. Default identity.\",\n} as const;\nconst BOX = {\n x: NUM('Left of the bounding box, document pixels.'),\n y: NUM('Top of the bounding box, document pixels.'),\n width: NUM('Width in pixels.'),\n height: NUM('Height in pixels.'),\n transform: { $ref: '#/$defs/matrix' },\n} as const;\n\n/** Shared parts, referenced as `#/$defs/...`. */\nconst DEFS = {\n matrix: MATRIX,\n point: {\n type: 'object',\n description: 'A point in document pixels.',\n properties: { x: NUM('Pixels from the left.'), y: NUM('Pixels from the top.') },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n arrowhead: {\n type: 'object',\n description: 'An arrowhead whose tip is the line end: a triangle, notched by concavity.',\n properties: {\n width: NUM('Width across the base, pixels.'),\n length: NUM('Length from base to tip, pixels.'),\n concavity: { type: 'number', minimum: -0.5, maximum: 0.5, description: 'Notch of the base towards the tip as a fraction of length (default 0).' },\n },\n required: ['width', 'length'],\n additionalProperties: false,\n },\n shape: {\n description:\n 'A live shape (stays editable as that shape): rect (with optional corner radii), ellipse, polygon, star, or line (a filled bar with optional arrowheads). Sizes are document pixels.',\n anyOf: [\n {\n type: 'object',\n description: 'Rectangle.',\n properties: {\n kind: { const: 'rect', description: 'Rectangle.' },\n ...BOX,\n radius: { type: 'number', minimum: 0, description: 'One corner radius for all corners, pixels.' },\n radii: { type: 'array', items: { type: 'number', minimum: 0, description: 'Radius in pixels.' }, minItems: 4, maxItems: 4, description: 'Corner radii [top-left, top-right, bottom-right, bottom-left] in pixels.' },\n },\n required: ['kind', 'x', 'y', 'width', 'height'],\n additionalProperties: false,\n },\n { type: 'object', description: 'Ellipse inscribed in the box.', properties: { kind: { const: 'ellipse', description: 'Ellipse.' }, ...BOX }, required: ['kind', 'x', 'y', 'width', 'height'], additionalProperties: false },\n {\n type: 'object',\n description: 'Regular polygon inscribed in the box, first vertex at the top.',\n properties: { kind: { const: 'polygon', description: 'Polygon.' }, ...BOX, sides: { type: 'integer', minimum: 3, maximum: 100, description: 'Number of sides (default 6).' } },\n required: ['kind', 'x', 'y', 'width', 'height'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Star inscribed in the box, first point at the top.',\n properties: {\n kind: { const: 'star', description: 'Star.' },\n ...BOX,\n points: { type: 'integer', minimum: 3, maximum: 100, description: 'Number of points (default 5).' },\n innerRatio: { type: 'number', minimum: 0.01, maximum: 1, description: 'Inner radius over outer radius (default 0.5).' },\n },\n required: ['kind', 'x', 'y', 'width', 'height'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Line drawn as a filled bar.',\n properties: {\n kind: { const: 'line', description: 'Line.' },\n x1: NUM('Start x, document pixels.'),\n y1: NUM('Start y, document pixels.'),\n x2: NUM('End x, document pixels.'),\n y2: NUM('End y, document pixels.'),\n weight: { type: 'number', minimum: 0, description: 'Thickness in pixels (default 1).' },\n startArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'Arrowhead at the start, or null.' },\n endArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'Arrowhead at the end, or null.' },\n transform: { $ref: '#/$defs/matrix' },\n },\n required: ['kind', 'x1', 'y1', 'x2', 'y2'],\n additionalProperties: false,\n },\n ],\n },\n subpaths: {\n description: 'A free path: SVG path data (absolute or relative M, L, C, Q, A, Z) or subpaths with anchors. Handles are absolute document positions.',\n anyOf: [\n { type: 'string', description: 'SVG path data, document pixels.' },\n {\n type: 'array',\n minItems: 1,\n description: 'Subpaths.',\n items: {\n type: 'object',\n description: 'One subpath.',\n properties: {\n anchors: {\n type: 'array',\n minItems: 1,\n description: 'Anchors in order.',\n items: {\n type: 'object',\n description: 'An anchor with optional handles.',\n properties: {\n x: NUM('Anchor x, document pixels.'),\n y: NUM('Anchor y, document pixels.'),\n in: { $ref: '#/$defs/point', description: 'Handle of the segment arriving here (absolute).' },\n out: { $ref: '#/$defs/point', description: 'Handle of the segment leaving here (absolute).' },\n corner: { type: 'number', minimum: 0, description: 'Live corner radius at this anchor, pixels.' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n closed: { type: 'boolean', description: 'Joins the last anchor back to the first (default true).' },\n },\n required: ['anchors'],\n additionalProperties: false,\n },\n },\n ],\n },\n gradient: {\n type: 'object',\n description: 'A gradient: colour stops, optional opacity stops and its layout (aligned with the shape bounds by default).',\n properties: {\n stops: {\n type: 'array',\n minItems: 1,\n description: 'Colour stops.',\n items: {\n type: 'object',\n description: 'A colour stop.',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, color: { type: 'string', description: 'Colour (#rrggbb or a CSS colour).' } },\n required: ['position', 'color'],\n additionalProperties: false,\n },\n },\n opacityStops: {\n type: 'array',\n description: 'Opacity stops (default opaque).',\n items: {\n type: 'object',\n description: 'An opacity stop.',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1.' } },\n required: ['position', 'opacity'],\n additionalProperties: false,\n },\n },\n style: { type: 'string', enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'], description: 'Geometry (default linear).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees, 90 = bottom to top (default 90).' },\n scale: { type: 'number', minimum: 0.1, maximum: 1.5, description: '1 = 100 % (default 1).' },\n reverse: { type: 'boolean', description: 'Swap the ends (default false).' },\n align: { type: 'boolean', description: 'Lay out over the shape bounds (true, default) or the canvas.' },\n offset: { $ref: '#/$defs/point', description: 'Centre offset as fractions of the bounds (default 0, 0).' },\n },\n required: ['stops'],\n additionalProperties: false,\n },\n paint: {\n description: 'What a fill or stroke paints with: a colour string (#rrggbb, #rrggbbaa, rgb(), a CSS name; alpha becomes opacity), or an object of type color, gradient or pattern.',\n anyOf: [\n { type: 'string', description: 'A colour.' },\n {\n type: 'object',\n description: 'Solid colour.',\n properties: { type: { const: 'color', description: 'Solid colour.' }, color: { type: 'string', description: 'Colour.' }, opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1 (default 1).' } },\n required: ['type', 'color'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Gradient.',\n properties: { type: { const: 'gradient', description: 'Gradient.' }, gradient: { $ref: '#/$defs/gradient' }, opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1 (default 1).' } },\n required: ['type', 'gradient'],\n additionalProperties: false,\n },\n {\n type: 'object',\n description: 'Pattern (list_patterns gives ids).',\n properties: {\n type: { const: 'pattern', description: 'Pattern.' },\n patternId: { type: 'string', description: 'A document or built-in pattern id.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 % (default 1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees counter-clockwise (default 0).' },\n link: { type: 'boolean', description: 'Tile from the shape’s top-left (default true) instead of the canvas origin.' },\n phase: { $ref: '#/$defs/point', description: 'Origin offset in pixels.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1 (default 1).' },\n },\n required: ['type', 'patternId'],\n additionalProperties: false,\n },\n ],\n },\n stroke: {\n type: 'object',\n description: 'The shape stroke (not the layer-effect stroke). Unset settings keep their current values (or the defaults).',\n properties: {\n width: { type: 'number', minimum: 0, maximum: 10000, description: 'Width in pixels (required for a new stroke).' },\n paint: { $ref: '#/$defs/paint' },\n color: { type: 'string', description: 'Shorthand for paint as a colour.' },\n align: { type: 'string', enum: ['inside', 'center', 'outside'], description: 'Where it sits on the outline (default center; open paths always centre).' },\n cap: { type: 'string', enum: ['butt', 'round', 'square'], description: 'Line ends (default butt).' },\n join: { type: 'string', enum: ['miter', 'round', 'bevel'], description: 'Corners (default miter).' },\n miterLimit: { type: 'number', minimum: 1, maximum: 500, description: 'Miter length over width above which a corner is bevelled (default 100).' },\n dashes: { type: 'array', maxItems: 32, items: { type: 'number', minimum: 0, description: 'Length in multiples of the width.' }, description: 'Dash and gap lengths in multiples of the stroke width ([] = solid).' },\n dashOffset: { type: 'number', description: 'Start offset into the dashes, multiples of the width (default 0).' },\n },\n additionalProperties: false,\n },\n} as const;\n\nconst GEOMETRY = {\n shape: { $ref: '#/$defs/shape', description: 'A live shape: { kind: rect | ellipse | polygon | star | line, … } in document pixels. Give shape or subpaths.' },\n subpaths: { $ref: '#/$defs/subpaths', description: 'A free path: SVG path data or subpaths with anchors, document pixels. Give shape or subpaths.' },\n} as const;\n\nfunction geometry(args: Record<string, unknown>): ShapeGeometryInput {\n const out: ShapeGeometryInput = {};\n if (args.shape !== undefined) out.shape = args.shape as ShapeGeometryInput['shape'];\n if (args.subpaths !== undefined) out.subpaths = args.subpaths as ShapeGeometryInput['subpaths'];\n if (args.op !== undefined) out.op = args.op as ShapeGeometryInput['op'];\n return out;\n}\n\n/**\n * Agent tools for shape layers in the image editor: `get_shape_layer`, `add_shape_layer`,\n * `update_shape` (fill, stroke, fill rule), `set_shape_component`, `add_shape_component`,\n * `remove_shape_component` and `merge_shape_components`. Every write is one undo step, validates\n * first and returns the shape as it is afterwards.\n */\nexport function createImageShapeTools(controller: ImageEditorController): AgentTool[] {\n const shapeId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a shape layer id from list_layers).');\n requireShapeLayer(controller.getLayers(), id);\n return id;\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 get: AgentTool = {\n name: 'get_shape_layer',\n title: 'Read a shape layer',\n description:\n 'Returns a shape layer’s components bottom to top (index, path operation, the live shape it was drawn as or null, its subpaths with absolute anchors and handles, bounds), the fill rule, fill and stroke paints and settings, the path bounds and the rendered bounds (stroke included). Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a shape layer', ...READ },\n handler: ({ layerId }) => controller.getShapeLayer(shapeId(layerId)),\n };\n\n const add: AgentTool = {\n name: 'add_shape_layer',\n title: 'Add a shape layer',\n description:\n 'Adds a vector shape layer and makes it active: a live shape (rect, ellipse, polygon, star, line), a free path (SVG path data or anchors), or several components combined with path operations. fill defaults to the foreground colour (none for an open path with a stroke); give stroke for an outline. It renders sharp at any size, can be moved and transformed without losing quality, and takes masks, effects, clipping and blend modes like any layer. Goes above the active layer unless placed. One undo step. Returns the layer and the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n ...GEOMETRY,\n components: {\n type: 'array',\n minItems: 1,\n description: 'Several components instead of shape / subpaths, bottom first, each with its path operation.',\n items: { type: 'object', description: 'One component.', properties: { ...GEOMETRY, op: OP }, additionalProperties: false },\n },\n fill: { anyOf: [{ $ref: '#/$defs/paint' }, { type: 'null', description: 'No fill.' }], description: 'Fill paint, or null for none (default the foreground colour).' },\n stroke: { anyOf: [{ $ref: '#/$defs/stroke' }, { type: 'null', description: 'No stroke.' }], description: 'Stroke { width, paint, … }, or null / omitted for none.' },\n fillRule: { type: 'string', enum: ['nonzero', 'evenodd'], description: 'Winding rule within a component (default nonzero).' },\n name: { type: 'string', description: 'Layer name (default by kind: Rectangle, Ellipse, …).' },\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 aboveId: { type: 'string', description: 'Put the layer directly above this layer (default above the active layer).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer.' },\n parentId: { type: 'string', description: 'Put the layer inside this group.' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a shape layer', ...WRITE },\n handler: (args) => {\n const init: ShapeLayerInit = { ...geometry(args) };\n if (args.components !== undefined) init.components = (args.components as Array<Record<string, unknown>>).map(geometry);\n for (const k of ['fill', 'stroke', 'fillRule', 'name', 'opacity', 'blend', 'aboveId', 'belowId', 'parentId', 'index'] as const) {\n if (args[k] !== undefined) (init as Record<string, unknown>)[k] = args[k];\n }\n if (init.blend !== undefined) init.blend = init.blend as BlendMode;\n const id = controller.addShapeLayer(init);\n return { layer: layerSummary(id), shape: controller.getShapeLayer(id) };\n },\n };\n\n const update: AgentTool = {\n name: 'update_shape',\n title: 'Restyle a shape',\n description:\n 'Changes a shape layer’s fill, stroke or fill rule and renders it again: fill replaces the fill (null removes it); stroke changes only the settings given (null removes the stroke; a new stroke needs width and paint). Geometry is untouched (set_shape_component edits paths). One undo step. Returns the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n fill: { anyOf: [{ $ref: '#/$defs/paint' }, { type: 'null', description: 'No fill.' }], description: 'New fill paint, or null for none.' },\n stroke: { anyOf: [{ $ref: '#/$defs/stroke' }, { type: 'null', description: 'No stroke.' }], description: 'Stroke settings to change, or null to remove the stroke.' },\n fillRule: { type: 'string', enum: ['nonzero', 'evenodd'], description: 'Winding rule within a component.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Restyle a shape', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, fill, stroke, fillRule }) => {\n const id = shapeId(layerId);\n if (fill === undefined && stroke === undefined && fillRule === undefined) throw new LayerOperationError('Give fill, stroke or fillRule to change.');\n return controller.setShapeStyle(id, { fill: fill as never, stroke: stroke as never, fillRule: fillRule as never });\n },\n };\n\n const setComponent: AgentTool = {\n name: 'set_shape_component',\n title: 'Edit a shape component',\n description:\n 'Changes one component of a shape layer: shape sets its live shape (fields you leave out keep their values when the kind stays the same, e.g. only radii), subpaths replaces its path (which ends the live shape), op changes its path operation. Positions are document pixels. One undo step. Returns the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n componentIndex: COMPONENT_INDEX,\n shape: {\n type: 'object',\n description: 'Live shape fields to set (see add_shape_layer’s shape): kind plus its fields; without kind, the fields change the current live shape.',\n properties: {\n kind: { type: 'string', enum: ['rect', 'ellipse', 'polygon', 'star', 'line'], description: 'Shape kind (default the current one).' },\n x: NUM('Left of the box, pixels.'),\n y: NUM('Top of the box, pixels.'),\n width: NUM('Box width, pixels.'),\n height: NUM('Box height, pixels.'),\n radius: { type: 'number', minimum: 0, description: 'rect: one corner radius, pixels.' },\n radii: { type: 'array', items: { type: 'number', minimum: 0, description: 'Radius in pixels.' }, minItems: 4, maxItems: 4, description: 'rect: [top-left, top-right, bottom-right, bottom-left] radii, pixels.' },\n sides: { type: 'integer', minimum: 3, maximum: 100, description: 'polygon: number of sides.' },\n points: { type: 'integer', minimum: 3, maximum: 100, description: 'star: number of points.' },\n innerRatio: { type: 'number', minimum: 0.01, maximum: 1, description: 'star: inner over outer radius.' },\n x1: NUM('line: start x.'),\n y1: NUM('line: start y.'),\n x2: NUM('line: end x.'),\n y2: NUM('line: end y.'),\n weight: { type: 'number', minimum: 0, description: 'line: thickness in pixels.' },\n startArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'line: start arrowhead or null.' },\n endArrow: { anyOf: [{ $ref: '#/$defs/arrowhead' }, { type: 'null', description: 'No arrowhead.' }], description: 'line: end arrowhead or null.' },\n transform: { $ref: '#/$defs/matrix' },\n },\n additionalProperties: false,\n },\n subpaths: { $ref: '#/$defs/subpaths', description: 'A new path for the component (SVG path data or anchors); ends its live shape.' },\n op: OP,\n },\n required: ['layerId', 'componentIndex'],\n additionalProperties: false,\n },\n annotations: { title: 'Edit a shape component', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, componentIndex, ...rest }) => {\n const id = shapeId(layerId);\n const change = geometry(rest);\n if (change.shape === undefined && change.subpaths === undefined && change.op === undefined) throw new LayerOperationError('Give shape, subpaths or op to change.');\n return controller.setShapeComponent(id, componentIndex as number, change);\n },\n };\n\n const addComponent: AgentTool = {\n name: 'add_shape_component',\n title: 'Add a shape component',\n description:\n 'Adds a component to a shape layer and combines it with the components below it by op: combine (union), subtract, intersect or exclude, as Photoshop’s path operations. Goes on top unless index is given. One undo step. Returns the shape.',\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: { layerId: LAYER_ID, ...GEOMETRY, op: OP, index: { type: 'integer', minimum: 0, description: 'Position among the components, 0 = bottom (default the top).' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a shape component', ...WRITE },\n handler: ({ layerId, index, ...rest }) => controller.addShapeComponent(shapeId(layerId), geometry(rest), index as number | undefined),\n };\n\n const removeComponent: AgentTool = {\n name: 'remove_shape_component',\n title: 'Delete a shape component',\n description: 'Deletes one component of a shape layer (the last one cannot go: delete the layer instead). One undo step. Returns the shape.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, componentIndex: COMPONENT_INDEX }, required: ['layerId', 'componentIndex'], additionalProperties: false },\n annotations: { title: 'Delete a shape component', ...DESTRUCTIVE },\n handler: ({ layerId, componentIndex }) => controller.removeShapeComponent(shapeId(layerId), componentIndex as number),\n };\n\n const merge: AgentTool = {\n name: 'merge_shape_components',\n title: 'Merge shape components',\n description:\n 'Merge Shape Components: the area a shape’s components combine to (after their path operations) becomes one plain component; live shapes become paths. The look is unchanged. One undo step. Returns the shape.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge shape components', ...DESTRUCTIVE },\n handler: ({ layerId }) => controller.mergeShapeComponents(shapeId(layerId)),\n };\n\n return [get, add, update, setComponent, addComponent, removeComponent, merge];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController, TextLayerInit } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError } from './layers/ops';\nimport { requireTypeLayer } from './layers/text';\nimport { describeLayer } 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 LAYER_ID = { type: 'string', description: 'Type layer id from list_layers (type type) or add_text_layer.' } as const;\nconst START = { type: 'integer', minimum: 0, description: 'Start of the range in UTF-16 code units, as get_text_layer counts them (default 0).' } as const;\nconst END = { type: 'integer', minimum: 0, description: 'End of the range, exclusive (default the end of the text).' } as const;\n\nconst STYLE = {\n fontFamily: { type: 'string', description: 'Font family name (list_fonts shows what is available; a missing family falls back, with a warning).' },\n fontWeight: { type: 'integer', minimum: 1, maximum: 1000, description: 'Weight 100..900 (400 regular, 700 bold).' },\n italic: { type: 'boolean', description: 'Italic.' },\n fontSize: { type: 'number', minimum: 0.5, maximum: 5000, description: 'Size in document pixels.' },\n color: { type: 'string', description: 'Colour (#rrggbb, or a CSS colour; its alpha becomes opacity).' },\n opacity: { type: ['number', 'null'], minimum: 0, maximum: 1, description: 'Text opacity 0..1 (null resets to 1).' },\n tracking: { type: ['number', 'null'], minimum: -1000, maximum: 10000, description: 'Letter spacing in 1/1000 em (null resets to 0).' },\n leading: { type: ['number', 'null'], minimum: 0, maximum: 10000, description: 'Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading × size).' },\n baselineShift: { type: ['number', 'null'], minimum: -5000, maximum: 5000, description: 'Pixels; positive raises the text (null resets to 0).' },\n underline: { type: 'boolean', description: 'Underline.' },\n strikethrough: { type: 'boolean', description: 'Strike through.' },\n} as const;\n\nconst PARAGRAPH = {\n align: { type: 'string', enum: ['left', 'center', 'right', 'justify'], description: 'Alignment (justify stretches paragraph-box lines but the last).' },\n autoLeading: { type: ['number', 'null'], minimum: 0.1, maximum: 10, description: 'Auto leading as a multiple of the font size (default 1.2).' },\n firstLineIndent: { type: ['number', 'null'], description: 'First-line indent in pixels.' },\n startIndent: { type: ['number', 'null'], description: 'Left indent in pixels.' },\n endIndent: { type: ['number', 'null'], description: 'Right indent in pixels.' },\n spaceBefore: { type: ['number', 'null'], description: 'Space above the paragraph in pixels (not before the first).' },\n spaceAfter: { type: ['number', 'null'], description: 'Space below the paragraph in pixels.' },\n} as const;\n\nfunction pick(args: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const k of keys) if (args[k] !== undefined) out[k] = args[k];\n return out;\n}\n\n/**\n * Agent tools for type layers in the image editor: `list_fonts`, `get_text_layer`,\n * `add_text_layer`, `set_text`, `set_text_style`, `set_paragraph_style`, `set_text_frame` and\n * `convert_text_to_shape`. Every write is one undo step, validates first and returns the layer as\n * it is afterwards (with any missing fonts).\n */\nexport function createImageTypeTools(controller: ImageEditorController): AgentTool[] {\n const typeId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a type layer id from list_layers).');\n requireTypeLayer(controller.getLayers(), id);\n return id;\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 const range = (args: Record<string, unknown>) => ({ start: args.start as number | undefined, end: args.end as number | undefined });\n\n const listFonts: AgentTool = {\n name: 'list_fonts',\n title: 'List fonts',\n description: 'Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List fonts', ...READ },\n handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null }),\n };\n\n const get: AgentTool = {\n name: 'get_text_layer',\n title: 'Read a type layer',\n description:\n 'Returns a type layer’s text, its character runs and paragraphs as [start, end) ranges in UTF-16 code units with their styles, the frame (point text, or a paragraph box with its size), the text-to-document transform, the laid-out lines (range, baseline, width), the rendered bounds, missing fonts and whether the pixels are live or stored from a file. Call before editing text you did not write, to get the ranges. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a type layer', ...READ },\n handler: ({ layerId }) => controller.getTextLayer(typeId(layerId)),\n };\n\n const add: AgentTool = {\n name: 'add_text_layer',\n title: 'Add a type layer',\n description:\n 'Adds a live type layer and makes it active: point text whose origin (x, y) is the first line’s baseline at its alignment edge, or paragraph text wrapped in box {width, height} with its top-left at (x, y). Line breaks in text start paragraphs. Style and paragraph settings apply to the whole text (change ranges later with set_text_style). Default style: 24 px sans-serif in the foreground colour. One undo step. Returns the layer and the text layer, including missingFonts.',\n inputSchema: {\n type: 'object',\n properties: {\n text: { type: 'string', description: 'The text; \\\\n starts a new paragraph.' },\n x: { type: 'number', description: 'Document x of the origin, pixels.' },\n y: { type: 'number', description: 'Document y of the origin (point text: the first baseline), pixels.' },\n box: {\n type: 'object',\n description: 'Make it paragraph (area) text wrapped in this box, pixels.',\n properties: { width: { type: 'number', minimum: 1, description: 'Box width, pixels.' }, height: { type: 'number', minimum: 1, description: 'Box height, pixels.' } },\n required: ['width', 'height'],\n additionalProperties: false,\n },\n style: { type: 'object', description: 'Character style.', properties: STYLE, additionalProperties: false },\n paragraph: { type: 'object', description: 'Paragraph style.', properties: PARAGRAPH, additionalProperties: false },\n rotation: { type: 'number', description: 'Degrees clockwise about the origin (default 0).' },\n name: { type: 'string', description: 'Layer name (default the start of the text).' },\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 aboveId: { type: 'string', description: 'Put the layer directly above this layer (default above the active layer).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer.' },\n parentId: { type: 'string', description: 'Put the layer inside this group.' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom.' },\n },\n required: ['text', 'x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a type layer', ...WRITE },\n handler: (args) => {\n const init = pick(args, ['text', 'x', 'y', 'box', 'style', 'paragraph', 'rotation', 'name', 'opacity', 'blend', 'aboveId', 'belowId', 'parentId', 'index']) as unknown as TextLayerInit;\n if (init.blend !== undefined) init.blend = init.blend as BlendMode;\n const id = controller.addTextLayer(init);\n return { layer: layerSummary(id), text: controller.getTextLayer(id) };\n },\n };\n\n const setText: AgentTool = {\n name: 'set_text',\n title: 'Edit text',\n description:\n 'Replaces the text in [start, end) (default all of it) with text; inserted text takes the style of the character at start. Use start = end to insert. Returns the text layer with its new runs.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, text: { type: 'string', description: 'Replacement text (\\\\n starts a paragraph; empty deletes the range).' }, start: START, end: END }, required: ['layerId', 'text'], additionalProperties: false },\n annotations: { title: 'Edit text', ...WRITE },\n handler: (args) => controller.setText(typeId(args.layerId), args.text as string, range(args)),\n };\n\n const setStyle: AgentTool = {\n name: 'set_text_style',\n title: 'Style a range of text',\n description:\n 'Sets character style fields (font, weight, italic, size, colour, tracking, leading, baseline shift, underline, strike-through) over [start, end) of a type layer (default all of it); fields not given keep their values per run. Returns the text layer with its runs and missingFonts.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, start: START, end: END, ...STYLE }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Style a range of text', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const style = pick(args, Object.keys(STYLE));\n if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(', ')}.`);\n return controller.setTextStyle(typeId(args.layerId), style, range(args));\n },\n };\n\n const setParagraph: AgentTool = {\n name: 'set_paragraph_style',\n title: 'Style paragraphs',\n description: 'Sets paragraph fields (alignment, auto leading, indents, spacing) on every paragraph that [start, end) touches (default all). Returns the text layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, start: START, end: END, ...PARAGRAPH }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Style paragraphs', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const style = pick(args, Object.keys(PARAGRAPH));\n if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(', ')}.`);\n return controller.setParagraphStyle(typeId(args.layerId), style, range(args));\n },\n };\n\n const setFrame: AgentTool = {\n name: 'set_text_frame',\n title: 'Point or paragraph text',\n description:\n 'Converts between point text (kind point: one line per paragraph) and paragraph text (kind box: wrapped in width × height pixels, its top-left where the first line starts), or resizes a paragraph box. The first line stays in place. Returns the text layer with its new lines.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: ['point', 'box'], description: 'point or box.' },\n width: { type: 'number', minimum: 1, description: 'Box width in pixels (kind box).' },\n height: { type: 'number', minimum: 1, description: 'Box height in pixels (kind box).' },\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Point or paragraph text', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, kind, width, height }) => {\n const id = typeId(layerId);\n if (kind === 'box' && (width === undefined || height === undefined)) throw new LayerOperationError('kind box needs width and height in pixels.');\n return controller.setTextFrame(id, kind === 'box' ? { kind: 'box', width: width as number, height: height as number } : { kind: 'point' });\n },\n };\n\n const toShape: AgentTool = {\n name: 'convert_text_to_shape',\n title: 'Convert text to a shape',\n description:\n 'Type › Convert to Shape: the text’s glyph outlines become a shape layer (same id, name, mask, effects, opacity, blend) that can be edited with the shape tools; the text is no longer editable. Needs a font shaper that gives outlines (the browser’s canvas cannot). The fill is the first run’s colour. One undo step. Returns the layer and warnings.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Convert text to a shape', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const { layerId: id, warnings } = controller.convertTextToShape(typeId(layerId));\n return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };\n },\n };\n\n return [listFonts, get, add, setText, setStyle, setParagraph, setFrame, toShape];\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 { createImageColorTools } from './color-tools';\nimport { createImageRetouchTools } from './retouch-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\nimport { createImageTransformTools } from './transform-tools';\nimport { createImageSmartObjectTools } from './smart-tools';\nimport { createImageShapeTools } from './shape-tools';\nimport { createImageTypeTools } from './type-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 * - Shape layers: `get_shape_layer`, `add_shape_layer`, `update_shape`, `set_shape_component`,\n * `add_shape_component`, `remove_shape_component`, `merge_shape_components` (see\n * `createImageShapeTools`); `rasterize_layer` turns one into pixels.\n * - Type layers: `list_fonts`, `get_text_layer`, `add_text_layer`, `set_text`, `set_text_style`,\n * `set_paragraph_style`, `set_text_frame`, `convert_text_to_shape` (see `createImageTypeTools`).\n * - Colour management: `get_color_profile`, `assign_profile`, `convert_to_profile`,\n * `set_proof_setup`, `convert_colors` (see `createImageColorTools`).\n * - Retouching: `clone_stamp`, `heal_brush`, `spot_heal`, `smudge`, `dodge`, `burn`, `sponge`,\n * `get_retouch_options`, `set_retouch_options`, `set_clone_source` (see `createImageRetouchTools`).\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 ...createImageShapeTools(controller),\n ...createImageTypeTools(controller),\n ...createImageEffectTools(controller),\n ...createImagePaintTools(controller),\n ...createImageColorTools(controller),\n ...createImageRetouchTools(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 * Framework-free logic behind the shape and type tools of the React and Svelte image editors:\n * tool options, a drag that draws a live shape (one undo step, the layer updated live while\n * dragging), and on-canvas text editing (caret, selection, typing, keys, styling the selection,\n * the overlay to draw). The components only translate DOM events and draw `overlay()`.\n */\n\nimport type { ImageEditorController, TypeLayerInfo } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { applyMatrix } from '../../geom/affine';\nimport { DEFAULT_TEXT_STYLE, caretAt, styleAt, textMatrix, typeLayerName, type TextLayout } from './text';\nimport { findLayer, type LiveShapeKind, type ParagraphStyle, type TextAlign, type TextStyle, type TypeLayer } from './model';\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/** What the shape tool draws next (and what its options bar edits on a selected shape). */\nexport interface ShapeToolOptions {\n kind: LiveShapeKind;\n /** `#rrggbb`, or `null` for no fill. */\n fill: string | null;\n /** `#rrggbb`, or `null` for no stroke. */\n stroke: string | null;\n strokeWidth: number;\n /** Rectangles: corner radius in pixels. */\n radius: number;\n /** Polygons: sides. */\n sides: number;\n /** Stars: points and inner radius ratio. */\n points: number;\n innerRatio: number;\n /** Lines: thickness in pixels and whether to add an arrowhead at the end. */\n weight: number;\n arrow: boolean;\n}\n\nexport const DEFAULT_SHAPE_TOOL_OPTIONS: ShapeToolOptions = {\n kind: 'rect',\n fill: '#4f46e5',\n stroke: null,\n strokeWidth: 2,\n radius: 0,\n sides: 6,\n points: 5,\n innerRatio: 0.5,\n weight: 4,\n arrow: false,\n};\n\nexport const SHAPE_KIND_LABELS: Record<LiveShapeKind, string> = { rect: 'Rectangle', ellipse: 'Ellipse', polygon: 'Polygon', star: 'Star', line: 'Line' };\n\n/** What the type tool sets new text in (and what its options bar edits on the selection). */\nexport interface TypeToolOptions {\n fontFamily: string;\n fontSize: number;\n fontWeight: number;\n italic: boolean;\n color: string;\n align: TextAlign;\n}\n\nexport const DEFAULT_TYPE_TOOL_OPTIONS: TypeToolOptions = { fontFamily: 'sans-serif', fontSize: 32, fontWeight: 400, italic: false, color: '#111827', align: 'left' };\n\n/** A live shape for a drag from `a` to `b` (Shift: square / circle / 45° line; Alt: from the centre). */\nexport function liveShapeForDrag(o: ShapeToolOptions, a: ImagePoint, b: ImagePoint, mods: { shift?: boolean; alt?: boolean } = {}): Record<string, unknown> | null {\n if (o.kind === 'line') {\n let x2 = b.x;\n let y2 = b.y;\n if (mods.shift) {\n const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n x2 = a.x + Math.cos(ang) * len;\n y2 = a.y + Math.sin(ang) * len;\n }\n if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;\n const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};\n return { kind: 'line', x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };\n }\n let w = b.x - a.x;\n let h = b.y - a.y;\n if (mods.shift) {\n const s = Math.max(Math.abs(w), Math.abs(h));\n w = Math.sign(w || 1) * s;\n h = Math.sign(h || 1) * s;\n }\n let x = a.x;\n let y = a.y;\n if (mods.alt) {\n x = a.x - w;\n y = a.y - h;\n w *= 2;\n h *= 2;\n }\n const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };\n if (box.width < 1 || box.height < 1) return null;\n switch (o.kind) {\n case 'rect':\n return { kind: 'rect', ...box, ...(o.radius > 0 ? { radius: o.radius } : {}) };\n case 'ellipse':\n return { kind: 'ellipse', ...box };\n case 'polygon':\n return { kind: 'polygon', ...box, sides: o.sides };\n case 'star':\n return { kind: 'star', ...box, points: o.points, innerRatio: o.innerRatio };\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Shape drawing\n// ---------------------------------------------------------------------------\n\n/**\n * A drag that draws a live shape: the layer is added on the first real move (one undo step) and\n * its component updated live (no history) until the pointer is released.\n */\nexport class ShapeDrawGesture {\n private start: ImagePoint | null = null;\n private layerId: string | null = null;\n error: string | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: () => ShapeToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The shape layer drawn by the last drag (until the next one starts). */\n get drawnLayerId(): string | null {\n return this.layerId;\n }\n\n down(p: ImagePoint): void {\n this.error = null;\n this.start = { x: p.x, y: p.y };\n this.layerId = null;\n }\n\n move(p: ImagePoint, mods: { shift?: boolean; alt?: boolean } = {}): void {\n if (!this.start) return;\n const o = this.options();\n const shape = liveShapeForDrag(o, this.start, p, mods);\n if (!shape) return;\n try {\n if (!this.layerId) {\n const isLine = o.kind === 'line';\n this.layerId = this.controller.addShapeLayer({\n shape,\n fill: isLine ? (o.stroke ?? o.fill ?? '#000000') : o.fill,\n stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null,\n });\n } else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n this.start = null;\n }\n }\n\n up(): string | null {\n this.start = null;\n return this.layerId;\n }\n\n cancel(): void {\n this.start = null;\n }\n}\n\n/** Applies options-bar changes to a shape layer (fill, stroke, width, and the live shape's own fields). */\nexport function applyShapeOptions(controller: ImageEditorController, layerId: string, change: Partial<ShapeToolOptions>): void {\n const info = controller.getShapeLayer(layerId);\n const style: Parameters<ImageEditorController['setShapeStyle']>[1] = {};\n if ('fill' in change) style.fill = change.fill ?? null;\n if ('stroke' in change || 'strokeWidth' in change) {\n const color = 'stroke' in change ? change.stroke : (info.stroke?.paint.type === 'color' ? info.stroke.paint.color : null);\n style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;\n }\n if (Object.keys(style).length) controller.setShapeStyle(layerId, style);\n const live = info.components[0]?.live;\n if (!live) return;\n const shape: Record<string, unknown> = {};\n if (live.kind === 'rect' && change.radius !== undefined) shape.radius = change.radius;\n if (live.kind === 'polygon' && change.sides !== undefined) shape.sides = change.sides;\n if (live.kind === 'star' && change.points !== undefined) shape.points = change.points;\n if (live.kind === 'star' && change.innerRatio !== undefined) shape.innerRatio = change.innerRatio;\n if (live.kind === 'line' && change.weight !== undefined) shape.weight = change.weight;\n if (live.kind === 'line' && change.arrow !== undefined) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;\n if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });\n}\n\n// ---------------------------------------------------------------------------\n// Text editing\n// ---------------------------------------------------------------------------\n\n/** The keys a text session understands, as DOM `KeyboardEvent`s report them. */\nexport interface TextKey {\n key: string;\n shiftKey?: boolean;\n metaKey?: boolean;\n ctrlKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Editing one type layer on the canvas: a caret and a selection (`anchor`..`focus`, UTF-16 code\n * units), typing, deleting, moving, clicking and styling the selection. The first change of a\n * session records one undo step; the rest join it.\n */\nexport class TextEditSession {\n anchor: number;\n focus: number;\n private changed = false;\n private readonly listeners = new Set<() => void>();\n error: string | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly layerId: string,\n index?: number\n ) {\n const n = this.text.length;\n this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));\n }\n\n private get layer(): TypeLayer {\n const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;\n if (!l || l.type !== 'type') throw new Error('The type layer being edited is gone.');\n return l;\n }\n\n get text(): string {\n return this.layer.text;\n }\n\n get start(): number {\n return Math.min(this.anchor, this.focus);\n }\n\n get end(): number {\n return Math.max(this.anchor, this.focus);\n }\n\n /** Whether the layer still exists (it can be deleted or undone away while editing). */\n get alive(): boolean {\n return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === 'type';\n }\n\n subscribe(fn: () => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private emit(): void {\n for (const l of this.listeners) l();\n }\n\n private layout(): TextLayout | null {\n return this.controller.getTextLayout(this.layerId);\n }\n\n private run<T>(fn: (commit: boolean) => T): T | undefined {\n this.error = null;\n try {\n const out = fn(!this.changed);\n this.changed = true;\n return out;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n return undefined;\n } finally {\n this.emit();\n }\n }\n\n /** Replaces the selection with `text` (typing and pasting). */\n insert(text: string): void {\n if (!text) return;\n const s = this.start;\n const e = this.end;\n const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));\n if (done) this.anchor = this.focus = s + text.replace(/\\r\\n?/g, '\\n').length;\n }\n\n /** Backspace: the selection, else the character before the caret. */\n deleteBackward(): void {\n if (this.start === this.end && this.start === 0) return;\n const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;\n const e = this.end;\n if (this.run((commit) => this.controller.setText(this.layerId, '', { start: s, end: e }, commit))) this.anchor = this.focus = s;\n }\n\n /** Delete: the selection, else the character after the caret. */\n deleteForward(): void {\n const n = this.text.length;\n if (this.start === this.end && this.end >= n) return;\n const s = this.start;\n const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;\n if (this.run((commit) => this.controller.setText(this.layerId, '', { start: s, end: e }, commit))) this.anchor = this.focus = s;\n }\n\n /** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */\n private surrogate(i: number, dir: 1 | -1): number {\n const t = this.text;\n const c = t.charCodeAt(i);\n if (dir < 0 && c >= 0xdc00 && c <= 0xdfff && i > 0) return 2;\n if (dir > 0 && c >= 0xd800 && c <= 0xdbff && i + 1 < t.length) return 2;\n return 1;\n }\n\n /** Moves the caret (Shift extends the selection). */\n move(to: 'left' | 'right' | 'up' | 'down' | 'home' | 'end' | 'start' | 'finish', extend = false): void {\n const n = this.text.length;\n let f = this.focus;\n const collapsedJump = !extend && this.start !== this.end;\n if (to === 'left') f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));\n else if (to === 'right') f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));\n else if (to === 'start') f = 0;\n else if (to === 'finish') f = n;\n else {\n const layout = this.layout();\n if (layout && layout.lines.length) {\n const c = caretAt(layout, f);\n const line = layout.lines[c.line];\n if (to === 'home') f = line.start;\n else if (to === 'end') {\n const last = line.chars[line.chars.length - 1];\n f = last ? Math.min(n, last.index + 1) : line.start;\n if (last && this.text[last.index] === ' ' && c.line < layout.lines.length - 1) f = last.index;\n } else {\n const target = layout.lines[c.line + (to === 'up' ? -1 : 1)];\n if (!target) f = to === 'up' ? 0 : n;\n else {\n const hit = target.chars.find((ch) => c.x < ch.x + ch.advance / 2);\n const last = target.chars[target.chars.length - 1];\n f = hit ? hit.index : last ? last.index + 1 : target.start;\n }\n }\n }\n }\n this.focus = f;\n if (!extend) this.anchor = f;\n this.emit();\n }\n\n selectAll(): void {\n this.anchor = 0;\n this.focus = this.text.length;\n this.emit();\n }\n\n /** Places the caret at a document point (Shift-click extends). */\n pointAt(p: ImagePoint, extend = false): void {\n this.focus = this.controller.textIndexAt(this.layerId, p);\n if (!extend) this.anchor = this.focus;\n this.emit();\n }\n\n /** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */\n handleKey(e: TextKey): boolean {\n const mod = e.metaKey || e.ctrlKey;\n if (mod && e.key.toLowerCase() === 'a') {\n this.selectAll();\n return true;\n }\n switch (e.key) {\n case 'Backspace':\n this.deleteBackward();\n return true;\n case 'Delete':\n this.deleteForward();\n return true;\n case 'Enter':\n this.insert('\\n');\n return true;\n case 'ArrowLeft':\n this.move(mod ? 'home' : 'left', e.shiftKey);\n return true;\n case 'ArrowRight':\n this.move(mod ? 'end' : 'right', e.shiftKey);\n return true;\n case 'ArrowUp':\n this.move(mod ? 'start' : 'up', e.shiftKey);\n return true;\n case 'ArrowDown':\n this.move(mod ? 'finish' : 'down', e.shiftKey);\n return true;\n case 'Home':\n this.move('home', e.shiftKey);\n return true;\n case 'End':\n this.move('end', e.shiftKey);\n return true;\n case 'Tab':\n this.insert('\\t');\n return true;\n }\n if (!mod && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {\n this.insert(e.key);\n return true;\n }\n return false;\n }\n\n /** Sets character style fields on the selection, or on the whole text when nothing is selected. */\n setStyle(style: Partial<TextStyle>): void {\n const s = this.start;\n const e = this.end;\n const range = s === e ? {} : { start: s, end: e };\n this.run((commit) => this.controller.setTextStyle(this.layerId, style as Partial<TextStyle> & Record<string, unknown>, range, commit));\n }\n\n /** Sets paragraph fields on the paragraphs the selection touches. */\n setParagraph(style: ParagraphStyle): void {\n const s = this.start;\n const e = this.end;\n this.run((commit) => this.controller.setParagraphStyle(this.layerId, style as ParagraphStyle & Record<string, unknown>, { start: s, end: e }, commit));\n }\n\n /** The style at the caret (or the selection's start), for the options bar. */\n styleHere(): TextStyle {\n const layer = this.layer;\n const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;\n return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);\n }\n\n info(): TypeLayerInfo {\n return this.controller.getTextLayer(this.layerId);\n }\n\n /** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */\n overlay(): { caret: [ImagePoint, ImagePoint] | null; selection: ImagePoint[][]; frame: ImagePoint[] | null } {\n const layer = this.layer;\n const layout = this.layout();\n const m = textMatrix(layer);\n const at = (x: number, y: number) => applyMatrix(m, { x, y });\n let frame: ImagePoint[] | null = null;\n if (layer.frame.kind === 'box') frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];\n else if (layout) {\n const b = layout.bounds;\n frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];\n }\n if (!layout) return { caret: null, selection: [], frame };\n const selection: ImagePoint[][] = [];\n if (this.start !== this.end) {\n for (const l of layout.lines) {\n const chars = l.chars.filter((c) => c.index >= this.start && c.index < this.end);\n if (!chars.length) continue;\n const x0 = chars[0].x;\n const x1 = chars[chars.length - 1].x + chars[chars.length - 1].advance;\n const top = l.baseline - l.ascent;\n const bottom = l.baseline + l.descent;\n selection.push([at(x0, top), at(x1, top), at(x1, bottom), at(x0, bottom)]);\n }\n }\n const c = caretAt(layout, this.focus);\n return { caret: [at(c.x, c.top), at(c.x, c.bottom)], selection, frame };\n }\n\n /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */\n close(): boolean {\n if (!this.alive) return false;\n if (!this.text.trim().length) {\n try {\n this.controller.removeLayer(this.layerId);\n } catch {\n // A locked or already removed layer stays as it is.\n }\n return false;\n }\n // New text is named after what was typed, as in Photoshop.\n if (this.layer.name === 'Text' && this.changed) {\n try {\n this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);\n } catch {\n // Keep the name.\n }\n }\n return true;\n }\n}\n\n/** Where a click lands on type layers: the topmost visible type layer whose laid-out text or box contains `p`, or `null`. */\nexport function typeLayerAt(controller: ImageEditorController, p: ImagePoint, slop = 4): string | null {\n const layers = [...flattenTypeLayers(controller.getLayers())].reverse();\n for (const l of layers) {\n if (!l.visible) continue;\n const inv = invert(textMatrix(l));\n const q = applyMatrix(inv, p);\n let b: { x: number; y: number; width: number; height: number } | null = null;\n if (l.frame.kind === 'box') b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };\n else {\n const layout = controller.getTextLayout(l.id);\n if (layout) b = layout.bounds;\n }\n if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;\n }\n return null;\n}\n\nfunction flattenTypeLayers(layers: ReturnType<ImageEditorController['getLayers']>): TypeLayer[] {\n const out: TypeLayer[] = [];\n const walk = (list: typeof layers) => {\n for (const l of list) {\n if (l.type === 'type') out.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n return out;\n}\n\nfunction invert(m: readonly number[]): [number, number, number, number, number, number] {\n const [a, b, c, d, e, f] = m;\n const det = a * d - b * c || 1;\n return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];\n}\n\n/**\n * The type tool: a click on type edits it at the clicked character; a click elsewhere adds point\n * text there (a drag adds paragraph text in the dragged box). Holds the open `TextEditSession`.\n */\nexport class TypeToolGesture {\n session: TextEditSession | null = null;\n private dragStart: ImagePoint | null = null;\n error: string | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: () => TypeToolOptions\n ) {}\n\n down(p: ImagePoint, mods: { shift?: boolean } = {}): void {\n this.error = null;\n const hit = typeLayerAt(this.controller, p);\n if (this.session && this.session.alive && hit === this.session.layerId) {\n this.session.pointAt(p, mods.shift);\n this.dragStart = null;\n return;\n }\n this.finish();\n if (hit) {\n this.controller.setActiveLayer(hit);\n this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));\n this.dragStart = null;\n return;\n }\n this.dragStart = { x: p.x, y: p.y };\n }\n\n /** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */\n up(p: ImagePoint): TextEditSession | null {\n const start = this.dragStart;\n this.dragStart = null;\n if (!start) return this.session;\n const o = this.options();\n const w = Math.abs(p.x - start.x);\n const h = Math.abs(p.y - start.y);\n try {\n const box = w >= 8 && h >= 8 ? { width: w, height: h } : undefined;\n const id = this.controller.addTextLayer({\n text: '',\n x: box ? Math.min(start.x, p.x) : start.x,\n y: box ? Math.min(start.y, p.y) : start.y,\n ...(box ? { box } : {}),\n style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },\n paragraph: o.align !== 'left' ? { align: o.align } : {},\n name: 'Text',\n });\n this.session = new TextEditSession(this.controller, id, 0);\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n }\n return this.session;\n }\n\n /** Ends the open session (an empty new layer is removed). */\n finish(): void {\n const s = this.session;\n this.session = null;\n s?.close();\n }\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 case 'shape':\n return 'shp';\n case 'type':\n return 'T';\n }\n}\n\n/** Whether the layer's row shows a pixel thumbnail (pixel, shape and type layers; shapes and type with a badge on it). */\nexport function layerHasThumbnail(layer: ImageLayer): boolean {\n return layer.type === 'pixel' || layer.type === 'shape' || layer.type === 'type';\n}\n\n/** A warning to show on a layer's row (a type layer drawn with fallback fonts), or `null`. */\nexport function layerWarning(layer: ImageLayer): string | null {\n if (layer.type === 'type' && layer.missingFonts?.length) {\n return `Missing ${layer.missingFonts.length === 1 ? 'font' : 'fonts'}: ${layer.missingFonts.join(', ')}. ${layer.rendering === 'stored' ? 'Showing the saved pixels until the text is edited.' : 'Drawn with a fallback font.'}`;\n }\n return null;\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' || active?.type === 'shape' || active?.type === 'type' || (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\nexport interface NormalizedLayerItem {\n id: string;\n name: string;\n imageUrl: string;\n thumbnailUrl?: string;\n visible: boolean;\n opacity: number;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Extracts a flat list of normalized layer metadata (0..1 bounds relative to document dimensions\n * and resolved PNG data-URL thumbnails) from the controller's current V3 layer state.\n */\nexport function extractNormalizedLayerItems(\n controller: ImageEditorController,\n fallbackImageUrl = ''\n): NormalizedLayerItem[] {\n const state = controller.getState();\n const ctrlLayers = controller.getLayers();\n if (!ctrlLayers || ctrlLayers.length === 0) return [];\n\n const serialized = controller.toJSON() as {\n assets?: Record<string, string>;\n layers?: Array<{ id: string; asset?: string; source?: string; children?: unknown[] }>;\n };\n const assets = serialized.assets ?? {};\n const allSerializedLayers: Array<{ id: string; asset?: string; source?: string; children?: unknown[] }> = [];\n\n const collectSerialized = (list: Array<{ id: string; asset?: string; source?: string; children?: unknown[] }>) => {\n for (const sl of list) {\n allSerializedLayers.push(sl);\n if (sl.children && Array.isArray(sl.children)) {\n collectSerialized(sl.children as Array<{ id: string; asset?: string; source?: string; children?: unknown[] }>);\n }\n }\n };\n collectSerialized(serialized.layers ?? []);\n\n const sLayerMap = new Map(allSerializedLayers.map((sl) => [sl.id, sl]));\n const imgW = state.imageDimensions?.width || 1;\n const imgH = state.imageDimensions?.height || 1;\n\n return ctrlLayers.map((l) => {\n const bounds = controller.getLayerBounds(l.id) || {\n x: 0,\n y: 0,\n width: imgW,\n height: imgH,\n };\n const sLayer = sLayerMap.get(l.id);\n const assetKey = sLayer?.asset;\n const layerDataUrl =\n (assetKey && assets[assetKey]) ||\n assets[l.id] ||\n (sLayer?.source === 'document' ? fallbackImageUrl : '');\n return {\n id: l.id,\n name: l.name || 'Untitled Layer',\n imageUrl: layerDataUrl || fallbackImageUrl,\n thumbnailUrl: layerDataUrl || undefined,\n visible: l.visible !== false,\n opacity: l.opacity ?? 1,\n x: imgW ? bounds.x / imgW : 0,\n y: imgH ? bounds.y / imgH : 0,\n width: imgW ? bounds.width / imgW : 1,\n height: imgH ? bounds.height / imgH : 1,\n };\n });\n}\n\n\n/** Image › Mode: the bit depths a panel offers, with their labels. */\nexport const BIT_DEPTH_OPTIONS: readonly { depth: 8 | 16 | 32; label: string; title: string }[] = [\n { depth: 8, label: '8-bit', title: '8 bits per channel' },\n { depth: 16, label: '16-bit', title: '16 bits per channel: smooth gradients, no banding under strong adjustments' },\n { depth: 32, label: '32-bit', title: '32-bit float per channel, linear light (HDR)' },\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","/**\n * The colour-management panel's view model, shared by the React and Svelte editors: the profile\n * choices for the working space and for Proof Setup, the rendering intents, and a snapshot of\n * the document's profile and proof for the panel to show.\n */\n\nimport { BUILTIN_PROFILES, type BuiltinProfileId, type Intent } from '../../cms';\nimport type { ImageEditorController } from '../controller';\n\n/** Profiles a document can work in (RGB). */\nexport const WORKING_SPACE_OPTIONS: readonly { id: BuiltinProfileId; label: string }[] = BUILTIN_PROFILES.filter((b) => b.colorSpace === 'rgb').map((b) => ({ id: b.id, label: b.description }));\n\n/** Profiles to soft-proof with (any colour space). */\nexport const PROOF_PROFILE_OPTIONS: readonly { id: BuiltinProfileId; label: string }[] = BUILTIN_PROFILES.map((b) => ({ id: b.id, label: b.description }));\n\nexport const INTENT_OPTIONS: readonly { id: Intent; label: string; title: string }[] = [\n { id: 'relative', label: 'Relative Colorimetric', title: 'Keep in-gamut colours exact, clip the rest (photos)' },\n { id: 'perceptual', label: 'Perceptual', title: 'Compress the whole gamut smoothly' },\n { id: 'saturation', label: 'Saturation', title: 'Keep colours vivid (graphics)' },\n { id: 'absolute', label: 'Absolute Colorimetric', title: 'Keep the source white too (paper proofs)' },\n];\n\n/** Image › Mode choices. */\nexport const COLOR_MODE_OPTIONS: readonly { id: 'rgb' | 'cmyk' | 'lab'; label: string; title: string }[] = [\n { id: 'rgb', label: 'RGB', title: 'Edit and export RGB in the working space' },\n { id: 'cmyk', label: 'CMYK', title: 'For print: proof through the CMYK profile and export CMYK' },\n { id: 'lab', label: 'Lab', title: 'Export Lab channels' },\n];\n\n/** What the colour panel shows. */\nexport function colorPanelView(controller: ImageEditorController) {\n const profile = controller.getColorProfile();\n const proof = controller.getState().proofSetup ?? null;\n return {\n profileName: profile.name,\n workingSpace: profile.builtin,\n mode: profile.mode,\n cmykProfileName: profile.cmykProfile?.name ?? null,\n proof: proof ? { id: proof.profile.builtin ?? null, name: proof.profile.name, intent: proof.intent, simulatePaper: proof.simulatePaper, gamutWarning: proof.gamutWarning, bpc: proof.bpc } : null,\n };\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"]}