@pixerate/editor 0.104.0 → 0.106.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 (31) hide show
  1. package/dist/{chunk-NQLQ2JOI.js → chunk-4X5MIVCE.js} +1155 -295
  2. package/dist/chunk-4X5MIVCE.js.map +1 -0
  3. package/dist/{chunk-VSMM7B3P.cjs → chunk-ZID226ZG.cjs} +1169 -309
  4. package/dist/chunk-ZID226ZG.cjs.map +1 -0
  5. package/dist/{compositor-CTMU-B1t.d.cts → compositor-Bc3PNP3C.d.cts} +1 -1
  6. package/dist/{compositor-B1ZrKR89.d.ts → compositor-BsYYllvR.d.ts} +1 -1
  7. package/dist/{export-X9rY79p0.d.cts → export-_m3Es4ru.d.cts} +19 -4
  8. package/dist/{export-BLpMyCYg.d.ts → export-iMrop3Jm.d.ts} +19 -4
  9. package/dist/image-editor/index.cjs +714 -539
  10. package/dist/image-editor/index.cjs.map +1 -1
  11. package/dist/image-editor/index.d.cts +50 -9
  12. package/dist/image-editor/index.d.ts +50 -9
  13. package/dist/image-editor/index.js +228 -53
  14. package/dist/image-editor/index.js.map +1 -1
  15. package/dist/{mixer-DGcH6HLu.d.cts → mixer-C1M1Nua_.d.cts} +1 -1
  16. package/dist/{mixer-DG_9Yu2W.d.ts → mixer-Cu5a9Bzd.d.ts} +1 -1
  17. package/dist/paint/index.d.cts +2 -2
  18. package/dist/paint/index.d.ts +2 -2
  19. package/dist/paint-editor/index.cjs +348 -32
  20. package/dist/paint-editor/index.cjs.map +1 -1
  21. package/dist/paint-editor/index.d.cts +154 -13
  22. package/dist/paint-editor/index.d.ts +154 -13
  23. package/dist/paint-editor/index.js +317 -1
  24. package/dist/paint-editor/index.js.map +1 -1
  25. package/dist/raster/index.d.cts +4 -4
  26. package/dist/raster/index.d.ts +4 -4
  27. package/dist/{surface-AfHCH7BR.d.cts → surface-BBJN9wxg.d.cts} +1 -1
  28. package/dist/{surface-AfHCH7BR.d.ts → surface-BBJN9wxg.d.ts} +1 -1
  29. package/package.json +1 -1
  30. package/dist/chunk-NQLQ2JOI.js.map +0 -1
  31. package/dist/chunk-VSMM7B3P.cjs.map +0 -1
@@ -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/migrate.ts","../../src/image-editor/layers/render.ts","../../src/image-editor/renderer.ts","../../src/image-editor/selection/shapes.ts","../../src/image-editor/selection/wand.ts","../../src/image-editor/selection/refine.ts","../../src/image-editor/free-transform.ts","../../src/image-editor/controller.ts","../../src/image-editor/document.ts","../../src/image-editor/controls.ts","../../src/image-editor/selection/tool-options.ts","../../src/image-editor/selection-tools.ts","../../src/image-editor/capture.ts","../../src/image-editor/edit-tools.ts","../../src/image-editor/layer-tools.ts","../../src/image-editor/effect-tools.ts","../../src/image-editor/layers/paint-ui.ts","../../src/image-editor/paint-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/transform-tools.ts","../../src/image-editor/smart-tools.ts","../../src/image-editor/tools.ts","../../src/image-editor/curve-editing.ts","../../src/image-editor/selection/gesture.ts","../../src/image-editor/mosaic-filter.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/effects-ui.ts","../../src/image-editor/psd/ui.ts","../../src/image-editor/layers/transform-ui.ts"],"names":["px","b","plain","data","out","pct","WRITE_IDEMPOTENT","LAYER_ID","opt","TARGET","summary","id","spec","bestD","q","partner"],"mappings":"AAAA;AACE;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACA;AC9bO,IAAM,eAAA,EAAiB,GAAA;AAgCvB,SAAS,gBAAA,CAAiB,SAAA,EAAwC;AACvE,EAAA,MAAM,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,CAAK,OAAA,EAAU,IAAA,CAAK,OAAA,EAAS,CAAA;AACzC,EAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,GAAA,EAAK,EAAA,GAAK,CAAA,EAAG;AAC/B,IAAA,GAAA,CAAI,CAAA,CAAE,IAAA,CAAK,EAAA,EAAI,CAAC,EAAA,EAAI,CAAA,CAAA,EAAI;AACtB,MAAA,WAAA,EAAA;AACA,MAAA,QAAA;AAAA,IACF;AACA,IAAA,OAAA,EAAA;AACA,IAAA,MAAM,EAAA,EAAI,SAAA,CAAU,IAAA,CAAK,CAAC,CAAC,CAAA;AAC3B,IAAA,MAAM,EAAA,EAAI,SAAA,CAAU,IAAA,CAAK,EAAA,EAAI,CAAC,CAAC,CAAA;AAC/B,IAAA,MAAM,EAAA,EAAI,SAAA,CAAU,IAAA,CAAK,EAAA,EAAI,CAAC,CAAC,CAAA;AAC/B,IAAA,GAAA,CAAI,CAAC,CAAA,EAAA;AACL,IAAA,KAAA,CAAM,CAAC,CAAA,EAAA;AACP,IAAA,IAAA,CAAK,CAAC,CAAA,EAAA;AACN,IAAA,SAAA,CAAU,IAAA,CAAK,KAAA,CAAM,OAAA,EAAS,EAAA,EAAI,OAAA,EAAS,EAAA,EAAI,OAAA,EAAS,CAAC,CAAC,CAAA,EAAA;AAC1D,IAAA,GAAA,CAAI,EAAA,IAAM,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,EAAA;AACtB,IAAA,GAAA,CAAI,EAAA,IAAM,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,EAAA;AACtB,IAAA,GAAA,CAAI,EAAA,IAAM,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,EAAA;AACtB,IAAA,GAAA,CAAI,EAAA,IAAM,GAAA,EAAK,UAAA,CAAW,CAAC,CAAA,EAAA;AAC3B,IAAA,GAAA,CAAI,EAAA,IAAM,GAAA,EAAK,UAAA,CAAW,CAAC,CAAA,EAAA;AAC3B,IAAA,GAAA,CAAI,EAAA,IAAM,GAAA,EAAK,UAAA,CAAW,CAAC,CAAA,EAAA;AAAA,EAC7B;AACA,EAAA,IAAI,IAAA,EAAM,CAAA;AACV,EAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,CAAC,GAAA,EAAK,KAAA,EAAO,IAAA,EAAM,SAAS,CAAA,EAAG;AAChD,IAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,cAAA,EAAgB,CAAA,EAAA,EAAK,GAAA,CAAI,IAAA,CAAK,CAAC,EAAA,EAAI,GAAA,EAAK,IAAA,EAAM,IAAA,CAAK,CAAC,CAAA;AAAA,EAC1E;AACA,EAAA,OAAO,EAAE,GAAA,EAAK,KAAA,EAAO,IAAA,EAAM,SAAA,EAAW,OAAA,EAAS,WAAA,EAAa,OAAA,EAAS,UAAA,EAAY,IAAI,CAAA;AACvF;AAEA,SAAS,SAAA,CAAU,CAAA,EAAmB;AACpC,EAAA,OAAO,EAAA,GAAK,EAAA,EAAI,EAAA,EAAI,EAAA,GAAK,IAAA,EAAM,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AACnD;AAQO,SAAS,aAAA,CACd,SAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,EACA,QAAA,EAAuD,CAAC,CAAA,EAChD;AACR,EAAA,MAAM,KAAA,EAAO,SAAA,CAAU,OAAO,CAAA;AAC9B,EAAA,MAAM,KAAA,EAAA,kBAAQ,OAAA,CAAQ,KAAA,UAAS,QAAA,EAAA,IAAY,MAAA;AAC3C,EAAA,MAAM,EAAA,EAAI,CAAC,CAAA,EAAA,GAAe,KAAA,EAAO,IAAA,CAAK,IAAA,CAAK,CAAC,EAAA,EAAI,CAAA;AAChD,EAAA,MAAM,IAAA,EAAM,CAAA,kBAAE,OAAA,CAAQ,GAAA,UAAO,SAAA,CAAU,KAAG,EAAA,GAAK,CAAA;AAC/C,EAAA,MAAM,GAAA,EAAK,MAAA,EAAA,CAAS,eAAA,EAAiB,CAAA,CAAA;AACrC,EAAA,IAAI,EAAA,EAAI,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA;AACA,EAAA;AACE,IAAA;AACH,IAAA;AACnB,EAAA;AACgB,EAAA;AAClB;AAE8B;ADkZP;AACA;AEnfP;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;AFueuB;AACA;AG/fV;AACA;AACA;AACiB;AA0BP;AAGP;AACF,EAAA;AACK,EAAA;AACC,EAAA;AACF,EAAA;AACG,EAAA;AACO,EAAA;AACL,EAAA;AACH,EAAA;AACG,EAAA;AACd,EAAA;AACT;AAMgB;AACE,EAAA;AACC,IAAA;AACF,IAAA;AACK,IAAA;AACpB,EAAA;AACO,EAAA;AACT;AAG6B;AAChB,EAAA;AACA,EAAA;AACI,IAAA;AACG,IAAA;AACG,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAM8B;AAChB,EAAA;AACd;AAGgB;AACI,EAAA;AACG,EAAA;AACvB;AAEgB;AACP,EAAA;AACD,IAAA;AACE,IAAA;AACG,IAAA;AACG,IAAA;AACF,IAAA;AACE,IAAA;AACF,IAAA;AACG,IAAA;AACK,IAAA;AACT,IAAA;AACX,EAAA;AACF;AAGgB;AACK,EAAA;AACZ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACX,EAAA;AACF;AAEgB;AACP,EAAA;AACC,IAAA;AACS,IAAA;AACT,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACP,IAAA;AACF,EAAA;AACF;AAEgB;AACD,EAAA;AACL,IAAA;AACI,IAAA;AACE,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACD,IAAA;AACO,IAAA;AACrB,EAAA;AACgB,EAAA;AACD,IAAA;AACE,IAAA;AACjB,EAAA;AACO,EAAA;AACT;AAGgB;AACY,EAAA;AAClB,IAAA;AACF,IAAA;AACE,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACE,IAAA;AACU,IAAA;AACX,IAAA;AACR,IAAA;AACF,EAAA;AACgB,EAAA;AACT,EAAA;AACT;AAGgB;AACU,EAAA;AACb,IAAA;AACK,IAAA;AACD,IAAA;AACf,EAAA;AACqB,EAAA;AACd,EAAA;AACT;AAGgB;AAEO,EAAA;AAEnB,EAAA;AAEgB,EAAA;AACc,EAAA;AACZ,EAAA;AAChB,EAAA;AACU,EAAA;AACE,IAAA;AACI,IAAA;AACpB,EAAA;AACoB,EAAA;AACP,EAAA;AACiB,EAAA;AACd,EAAA;AACA,EAAA;AACU,EAAA;AACtB,EAAA;AACG,EAAA;AACT;AAOgB;AACuB,EAAA;AAC1B,IAAA;AACE,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACC,IAAA;AACf,EAAA;AACmB,EAAA;AACD,EAAA;AACX,EAAA;AACT;AAG8B;AACE,EAAA;AAChB,EAAA;AACI,IAAA;AACC,MAAA;AACJ,MAAA;AACb,IAAA;AACF,EAAA;AACW,EAAA;AACQ,EAAA;AACF,IAAA;AACI,IAAA;AACrB,EAAA;AACoB,EAAA;AACtB;AAYI;AAGuB;AACN,EAAA;AACJ,EAAA;AACC,EAAA;AACG,EAAA;AACmB,EAAA;AACjC,IAAA;AAEC,IAAA;AACc,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACC,MAAA;AAEf,IAAA;AACL,IAAA;AACe,IAAA;AACI,IAAA;AACrB,EAAA;AACyC,EAAA;AACN,EAAA;AACrB,EAAA;AACI,IAAA;AACC,MAAA;AACJ,MAAA;AACA,MAAA;AACb,IAAA;AACF,EAAA;AACW,EAAA;AACU,EAAA;AACD,EAAA;AACgB,EAAA;AAG3B,EAAA;AACX;AAG8B;AACT,EAAA;AACA,EAAA;AACL,EAAA;AACA,EAAA;AACK,EAAA;AACX,EAAA;AACW,EAAA;AACE,EAAA;AACD,EAAA;AACC,EAAA;AACH,EAAA;AACL,EAAA;AACE,IAAA;AACM,IAAA;AACA,IAAA;AACrB,EAAA;AACQ,EAAA;AACU,EAAA;AACA,EAAA;AACE,IAAA;AACC,IAAA;AACnB,IAAA;AACF,EAAA;AACkB,EAAA;AACE,IAAA;AACH,IAAA;AACjB,EAAA;AACY,EAAA;AACF,EAAA;AACH,EAAA;AACT;AH+auB;AACA;AIzwBO;AACrB,EAAA;AACT;AAEuB;AACP,EAAA;AAChB;AAGgB;AACP,EAAA;AACL,IAAA;AACO,IAAA;AACU,IAAA;AACA,IAAA;AACV,IAAA;AACU,IAAA;AACA,IAAA;AACnB,EAAA;AACF;AAOgB;AAME,EAAA;AACG,EAAA;AACA,EAAA;AACH,EAAA;AACN,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACA,EAAA;AACC,EAAA;AACD,IAAA;AACA,IAAA;AACD,IAAA;AACG,MAAA;AACF,MAAA;AACA,MAAA;AACD,MAAA;AACG,MAAA;AACA,MAAA;AACA,MAAA;AACnB,IAAA;AACF,EAAA;AACmB,EAAA;AACrB;AAGgB;AACG,EAAA;AACD,IAAA;AACE,IAAA;AAClB,EAAA;AACI,EAAA;AACN;AA2BgB;AACC,EAAA;AACI,EAAA;AACF,EAAA;AACE,EAAA;AACE,EAAA;AACX,EAAA;AACG,EAAA;AACI,EAAA;AACF,IAAA;AACC,IAAA;A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A;AACE,IAAA;AACb,IAAA;AACU,IAAA;AACK,IAAA;AACH,IAAA;AACT,IAAA;AACW,IAAA;AACA,IAAA;AACT,IAAA;AACX,EAAA;AACO,EAAA;AACT;APqgEuB;AACA;AQlyEV;AACH,EAAA;AACK,EAAA;AACL,EAAA;AACC,EAAA;AACC,EAAA;AACC,EAAA;AACb;AAGa;AAE2B;AAGxB;AACP,EAAA;AACY,IAAA;AACF,IAAA;AACE,IAAA;AACC,IAAA;AACC,IAAA;AACH,IAAA;AAClB,EAAA;AACF;AAEoD;AAC/B;AACC;AACD;AACL,EAAA;AACA,EAAA;AAChB;AAGgB;AACJ,EAAA;AACU,EAAA;AACL,EAAA;AAGR,EAAA;AAKT;AAGgD;AAC9B,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAClB,IAAA;AACF,EAAA;AACgB,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACH,MAAA;AACf,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAOgB;AAQA,EAAA;AACO,EAAA;AACeC,EAAAA;AACpB,IAAA;AACE,IAAA;AACT,IAAA;AACT,EAAA;AACmB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACM,EAAA;AACA,IAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACD,IAAA;AACG,MAAA;AACA,MAAA;AACA,MAAA;AACnB,IAAA;AACQ,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AACnB,EAAA;AACoB,EAAA;AACA,EAAA;AACA,EAAA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ACA,QAAA;AACF,QAAA;AACD,UAAA;AACK,UAAA;AACf,QAAA;AACa,QAAA;AACf,MAAA;AACF,IAAA;AACgB,IAAA;AAC+B,IAAA;AACpC,MAAA;AACT,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACe,MAAA;AACE,MAAA;AACD,MAAA;AACF,MAAA;AAChB,IAAA;AACe,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACZ,IAAA;AACT,EAAA;AACF;AAMS;AAC4B,EAAA;AACnB,EAAA;AACA,EAAA;AACZ,EAAA;AACS,IAAA;AACI,IAAA;AACjB,EAAA;AACO,EAAA;AACT;AAGqB;AACE,EAAA;AACL,EAAA;AACF,EAAA;AACI,IAAA;AACJ,MAAA;AACJ,MAAA;AACG,MAAA;AACG,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACG,MAAA;AACV,IAAA;AACG,IAAA;AACN,EAAA;AACmB,EAAA;AACH,EAAA;AACK,IAAA;AACF,IAAA;AACnB,EAAA;AACiB,EAAA;AACJ,EAAA;AACM,EAAA;AACrB;AAGkB;AACC,EAAA;AACe,EAAA;AAChB,EAAA;AACD,IAAA;AACG,MAAA;AACR,MAAA;AACG,MAAA;AACT,MAAA;AACD,IAAA;AACH,EAAA;AACkB,EAAA;AACE,EAAA;AACH,EAAA;AACV,EAAA;AACT;AAGS;AACM,EAAA;AACR,IAAA;AACU,IAAA;AACM,IAAA;AACH,IAAA;AACA,IAAA;AACF,IAAA;AACD,IAAA;AACf,EAAA;AACkB,EAAA;AACb,EAAA;AACa,IAAA;AACH,IAAA;AACE,IAAA;AACD,MAAA;AACD,QAAA;AACA,QAAA;AACA,QAAA;AACV,MAAA;AACI,IAAA;AACK,MAAA;AACE,MAAA;AACK,MAAA;AACnB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGuB;AACS,EAAA;AACpB,EAAA;AACW,IAAA;AACD,IAAA;AACD,IAAA;AACnB,EAAA;AACU,EAAA;AACW,IAAA;AACP,IAAA;AACK,IAAA;AACnB,EAAA;AACU,EAAA;AACW,IAAA;AACT,IAAA;AACZ,EAAA;AACqB,EAAA;AACD,IAAA;AACC,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAGS;AACa,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACH,EAAA;AACP,EAAA;AACM,EAAA;AACC,IAAA;AACD,IAAA;AACD,IAAA;AACF,IAAA;AACI,IAAA;AACjB,EAAA;AACO,EAAA;AACT;AAGS;AACS,EAAA;AACI,EAAA;AACP,IAAA;AACK,IAAA;AAClB,EAAA;AACO,EAAA;AACT;AAES;AACS,EAAA;AACC,IAAA;AACE,IAAA;AACF,IAAA;AACE,MAAA;AACT,MAAA;AACS,MAAA;AACA,MAAA;AACjB,IAAA;AACF,EAAA;AACF;AAGoB;AACS,EAAA;AACX,EAAA;AACC,IAAA;AACA,IAAA;AACJ,IAAA;AACQ,MAAA;AACR,MAAA;AACC,MAAA;AACK,IAAA;AACnB,EAAA;AACO,EAAA;AACT;AAG6BA;AACZ,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACM,EAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACC,QAAA;AACA,QAAA;AACN,QAAA;AACX,MAAA;AACF,IAAA;AACF,EAAA;AACgB,EAAA;AAClB;AAES;AACoB,EAAA;AACX,EAAA;AACC,IAAA;AACJ,IAAA;AACb,EAAA;AACO,EAAA;AACT;AAG2B;AACP,EAAA;AACG,IAAA;AACP,MAAA;AACZ,IAAA;AACmB,IAAA;AACrB,EAAA;AACU,EAAA;AACQ,EAAA;AACL,EAAA;AACS,EAAA;AACG,EAAA;AACL,EAAA;AACtB;AAGoD;AAC9B,EAAA;AACtB;AAGS;AACM,EAAA;AACf;AAEuB;AACH,EAAA;AACN,EAAA;AACA,EAAA;AACQ,EAAA;AACb,EAAA;AACT;AAGS;AACa,EAAA;AACF,EAAA;AACC,EAAA;AACZ,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACA,IAAA;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,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACI,IAAA;AACN,MAAA;AACK,MAAA;AACC,MAAA;AACR,MAAA;AACX,IAAA;AACF,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACH,IAAA;AACC,MAAA;AACF,MAAA;AACX,IAAA;AACF,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AjBowOuB;AACA;AkBh2PR;AACC;AACVD;AACgB;AAEH;AACC;AACF;AAGL;AACI,EAAA;AACN,EAAA;AACD,IAAA;AACO,IAAA;AACD,IAAA;AACI,MAAA;AACN,MAAA;AACA,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACD,YAAA;AACC,YAAA;AACA,YAAA;AACZ,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACmB,IAAA;AACnB,IAAA;AACF,EAAA;AACU,EAAA;AACF,IAAA;AACO,IAAA;AACD,IAAA;AACH,MAAA;AACC,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACD,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACc,MAAA;AACN,QAAA;AACO,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACA,YAAA;AACC,YAAA;AACb,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACe,MAAA;AACA,MAAA;AACA,M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AA;AACd,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACJ,IAAA;AACO,IAAA;AACK,EAAA;AACL,IAAA;AACT,EAAA;AACF;AAcgB;AACK,EAAA;AACsB,EAAA;AACL,EAAA;AAC1B,IAAA;AACR,IAAA;AACkB,IAAA;AACA,IAAA;AAClB,EAAA;AACkB,EAAA;AACtB;A5BwnTuB;AACA;A6Bn1TP;AACE,EAAA;AACK,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACV,EAAA;AACM,IAAA;AACX,IAAA;AACmB,IAAA;AACN,IAAA;AACF,IAAA;AACA,IAAA;AACb,EAAA;AACF;AAGgB;AACM,EAAA;AACb,EAAA;AACT;AAGa;AAQE;AACC;AAEP;AACa,EAAA;AACtB;AAK6B;AACV,EAAA;AACI,EAAA;AACD,IAAA;AACC,IAAA;AACE,IAAA;AACvB,EAAA;AACO,EAAA;AACT;AAG6B;AACR,EAAA;AACE,EAAA;AACF,EAAA;AACD,IAAA;AACC,IAAA;AACE,IAAA;AACZ,IAAA;AACT,EAAA;AACiB,EAAA;AACG,EAAA;AACtB;AAEsB;AACN,EAAA;AACuB,EAAA;AACtB,EAAA;AACK,EAAA;AACf,EAAA;AACC,IAAA;AACK,MAAA;AACA,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAES;AACA,EAAA;AACW,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAClB,EAAA;AACF;AAEwC;AACC;AACA;AAMzB;AAEV,EAAA;AAGoB,IAAA;AACD,IAAA;AACjB,EAAA;AACU,EAAA;AACT,IAAA;AACA,IAAA;AACI,MAAA;AACF,QAAA;AACI,QAAA;AACA,QAAA;AACM,QAAA;AACE,QAAA;AACJ,QAAA;AACA,QAAA;AACE,QAAA;AACE,QAAA;AACjB,MAAA;AACG,IAAA;AACA,IAAA;AACI,M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,EAAA;AACpB,IAAA;AACA,IAAA;AACuB,IAAA;AACvB,IAAA;AACA,IAAA;AACK,MAAA;AACH,MAAA;AAChB,IAAA;AACiB,IAAA;AACA,IAAA;AACH,IAAA;AACH,IAAA;AACA,IAAA;AACC,IAAA;AACN,IAAA;AACT,EAAA;AAAA;AAGe,EAAA;AACI,oBAAA;AACC,IAAA;AACH,IAAA;AACH,IAAA;AACA,IAAA;AACG,IAAA;AACF,IAAA;AACA,IAAA;AACf,EAAA;AAAA;AAAA;AAAA;AAAA;AAM+B,EAAA;AACjB,IAAA;AACG,IAAA;AACG,IAAA;AACT,MAAA;AACM,QAAA;AACJ,QAAA;AACT,MAAA;AACO,MAAA;AACT,IAAA;AACc,IAAA;AACA,MAAA;AACL,MAAA;AACT,IAAA;AACc,IAAA;AACA,MAAA;AACL,MAAA;AACT,IAAA;AACmB,IAAA;AACJ,IAAA;AACuB,IAAA;AACpB,IAAA;AACJ,IAAA;AACI,MAAA;AACT,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACF;A/By6TuB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","sourcesContent":[null,"// Portions ported from photocraft-main/crates/algo/src/histogram.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Display bins of a histogram. */\nexport const HISTOGRAM_BINS = 256;\n\nexport interface ImageHistogram {\n /** 256-bin counts per channel. */\n red: Uint32Array;\n green: Uint32Array;\n blue: Uint32Array;\n /** 256-bin counts of Rec.709 luma `round(0.2126 R + 0.7152 G + 0.0722 B)` of the encoded values. */\n luminance: Uint32Array;\n /** Visible pixels counted (alpha > 0). Each counts once, partially transparent ones included. */\n samples: number;\n /** Fully transparent pixels, excluded from every bin. */\n transparent: number;\n /** Per channel (R, G, B): visible pixels at exactly 0. */\n shadows: [number, number, number];\n /** Per channel (R, G, B): visible pixels at exactly 255. */\n highlights: [number, number, number];\n /** Largest bin count over R, G, B and luminance (for scaling a plot). */\n max: number;\n}\n\ninterface PixelSource {\n data: ArrayLike<number>;\n width?: number;\n height?: number;\n}\n\n/**\n * Per-channel and luminance histograms of straight 8-bit RGBA pixels (an `ImageData` or anything\n * with `data`), with clipping counts at the exact endpoints. Transparent pixels cannot add\n * phantom peaks: they are counted separately and excluded.\n */\nexport function computeHistogram(imageData: PixelSource): ImageHistogram {\n const red = new Uint32Array(HISTOGRAM_BINS);\n const green = new Uint32Array(HISTOGRAM_BINS);\n const blue = new Uint32Array(HISTOGRAM_BINS);\n const luminance = new Uint32Array(HISTOGRAM_BINS);\n const shadows: [number, number, number] = [0, 0, 0];\n const highlights: [number, number, number] = [0, 0, 0];\n let samples = 0;\n let transparent = 0;\n const data = imageData.data;\n const len = data.length - (data.length % 4);\n for (let i = 0; i < len; i += 4) {\n if (!(data[i + 3] > 0)) {\n transparent++;\n continue;\n }\n samples++;\n const r = clampByte(data[i]);\n const g = clampByte(data[i + 1]);\n const b = clampByte(data[i + 2]);\n red[r]++;\n green[g]++;\n blue[b]++;\n luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) shadows[0]++;\n if (g === 0) shadows[1]++;\n if (b === 0) shadows[2]++;\n if (r === 255) highlights[0]++;\n if (g === 255) highlights[1]++;\n if (b === 255) highlights[2]++;\n }\n let max = 0;\n for (const bins of [red, green, blue, luminance]) {\n for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];\n }\n return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };\n}\n\nfunction clampByte(v: number): number {\n return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);\n}\n\nexport type HistogramChannel = 'red' | 'green' | 'blue' | 'luminance';\n\n/**\n * An SVG path (`M … Z`) of one channel scaled into a `width` × `height` box, bins left to right,\n * counts up from the bottom. `scale: 'sqrt'` (default) keeps small populations visible.\n */\nexport function histogramPath(\n histogram: ImageHistogram,\n channel: HistogramChannel,\n width: number,\n height: number,\n options: { scale?: 'linear' | 'sqrt'; max?: number } = {}\n): string {\n const bins = histogram[channel];\n const sqrt = (options.scale ?? 'sqrt') === 'sqrt';\n const f = (v: number) => (sqrt ? Math.sqrt(v) : v);\n const top = f(options.max ?? histogram.max) || 1;\n const dx = width / (HISTOGRAM_BINS - 1);\n let d = `M0 ${height}`;\n for (let k = 0; k < HISTOGRAM_BINS; k++) {\n const y = height - (f(bins[k]) / top) * height;\n d += ` L${round2(k * dx)} ${round2(y)}`;\n }\n return `${d} L${round2(width)} ${height} Z`;\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { SelectionMask } from './mask';\n\n/**\n * The selection's outline at `threshold` (0..255, default 128) as axis-aligned segments on pixel\n * boundaries, `[x0, y0, x1, y1, …]` in natural image pixels, with collinear neighbours merged.\n * This is what marching ants are drawn from. Linear in the pixel count.\n */\nexport function selectionOutline(mask: SelectionMask, threshold = 128): Float32Array {\n const { width: w, height: h, data } = mask;\n const segs: number[] = [];\n if (!mask.bounds) return new Float32Array(0);\n const b = mask.bounds;\n const inside = (x: number, y: number) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;\n // Horizontal boundaries: the line y separates rows y − 1 and y.\n for (let y = b.y; y <= b.y + b.height; y++) {\n let start = -1;\n for (let x = b.x; x <= b.x + b.width; x++) {\n const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);\n if (edge && start < 0) start = x;\n else if (!edge && start >= 0) {\n segs.push(start, y, x, y);\n start = -1;\n }\n }\n }\n // Vertical boundaries: the line x separates columns x − 1 and x.\n for (let x = b.x; x <= b.x + b.width; x++) {\n let start = -1;\n for (let y = b.y; y <= b.y + b.height; y++) {\n const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);\n if (edge && start < 0) start = y;\n else if (!edge && start >= 0) {\n segs.push(x, start, x, y);\n start = -1;\n }\n }\n }\n return Float32Array.from(segs);\n}\n\nexport interface QuickMaskStyle {\n /** Overlay colour, `[r, g, b]` 0..255 (default Photoshop's rubylith red). */\n color?: readonly [number, number, number];\n /** 0..1 (default 0.5): opacity over fully unselected pixels. */\n opacity?: number;\n}\n\n/**\n * Quick Mask view: RGBA pixels (natural image size) tinting the *unselected* area, so the\n * selection shows through clear, as Photoshop's rubylith overlay.\n */\nexport function quickMaskPixels(mask: SelectionMask, style: QuickMaskStyle = {}): Uint8ClampedArray {\n const [r, g, b] = style.color ?? [255, 0, 0];\n const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n out[p] = r;\n out[p + 1] = g;\n out[p + 2] = b;\n out[p + 3] = Math.round((255 - mask.data[i]) * k);\n }\n return out;\n}\n","/**\n * Between the old document-level model (schema v1 / v2: base sliders, adjustment layers, blur,\n * depth mask and annotations) and the layer stack:\n *\n * - `layersFromLegacy` lifts a v1 / v2 state into layers: the source becomes the Background\n * pixel layer carrying the base sliders as its `develop` filter and the depth mask as its\n * parametric mask; adjustment layers go above it, annotations on top in their order. Blur\n * becomes a `blur` filter on the Background, or `legacyOutputBlur` when adjustment layers\n * exist (blur used to run after them).\n * - `legacyView` reads the stack back as `adjustments` / `depthMask` / `annotations` (the\n * compatibility views on the editor state) and says whether the stack still has exactly the\n * legacy shape, in which case it serializes as v1 / v2 byte for byte.\n */\n\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport { normalizeAdjustmentLayers } from '../adjustment-layers';\nimport { normalizeProcessVersion } from '../color';\nimport { decodeLayerMask, normalizeLayerMask } from '../selection/layer-mask';\nimport { encodeSelectionRle, resizeSelection } from '../selection/mask';\nimport type {\n AdjustmentLayer,\n AdjustmentLayerMask,\n Annotation,\n DepthMaskOptions,\n ImageAdjustments,\n} from '../types';\nimport {\n blurFilter,\n developFilter,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { emptySurface, readRegion, surfaceFromPixels, type MaskSurface } from '../../raster/surface';\n\nexport const BACKGROUND_LAYER_ID = 'background';\nexport const BACKGROUND_LAYER_NAME = 'Background';\nexport const DEVELOP_FILTER_ID = 'develop';\nexport const BLUR_FILTER_ID = 'blur';\n\n/** The old document-level parts of the editor state. */\nexport interface LegacyParts {\n adjustments: ImageAdjustments;\n depthMask: DepthMaskOptions;\n annotations: Annotation[];\n}\n\n/** The stack as stored at document level next to the layers. */\nexport interface LayerStack {\n layers: ImageLayer[];\n /** Export alpha (the old `adjustments.opacity`), 0..1. */\n outputOpacity: number;\n /** Migrated blur that must run after the adjustment layers (never created by new edits). */\n legacyOutputBlur?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Masks\n// ---------------------------------------------------------------------------\n\n/**\n * Stored `r1:` encodings of masks lifted from v1 / v2, by surface, so an unchanged mask writes\n * back byte for byte (and a mask of another size than the document keeps its size).\n */\nconst legacyMaskMemo = new WeakMap<MaskSurface, { width: number; height: number; rle: string }>();\n\n/** A raster mask from an old adjustment-layer mask, resampled to the document if needed. */\nexport function rasterMaskFromLegacy(mask: AdjustmentLayerMask, width: number, height: number): RasterMask | null {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);\n legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });\n const out: RasterMask = { surface, defaultValue: 0 };\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\n/**\n * True when an adjustment layer's mask came from a v1 / v2 mask of another size than the canvas\n * (the old renderer resampled it per render; the stack holds a canvas-sized copy).\n */\nexport function hasResampledLegacyMask(layers: readonly ImageLayer[], width: number, height: number): boolean {\n for (const l of layers) {\n if (l.type !== 'adjustment' || !l.mask) continue;\n const memo = legacyMaskMemo.get(l.mask.surface);\n if (memo && (memo.width !== width || memo.height !== height)) return true;\n }\n return false;\n}\n\n/** The old mask shape of a raster mask (its pixels over the canvas, run-length encoded). */\nexport function legacyMaskOf(mask: RasterMask, width: number, height: number): AdjustmentLayerMask {\n let memo = legacyMaskMemo.get(mask.surface);\n if (!memo) {\n const data = readRegion(mask.surface, { x: 0, y: 0, width, height });\n memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };\n legacyMaskMemo.set(mask.surface, memo);\n }\n return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled })!;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy → layers\n// ---------------------------------------------------------------------------\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** True when the depth mask settings are the defaults (no mask at all). */\nexport function isDefaultDepthMask(mask: DepthMaskOptions | undefined): boolean {\n if (!mask) return true;\n return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...(mask.depthRange ?? [])] }, DEFAULT_DEPTH_MASK_VALUES);\n}\n\nexport function developFromAdjustments(adjustments: ImageAdjustments): DevelopFilter {\n return {\n id: DEVELOP_FILTER_ID,\n kind: 'develop',\n enabled: true,\n brightness: adjustments.brightness ?? 0,\n contrast: adjustments.contrast ?? 0,\n saturation: adjustments.saturation ?? 0,\n exposure: adjustments.exposure ?? 0,\n temperature: adjustments.temperature ?? 0,\n tint: adjustments.tint ?? 0,\n process: normalizeProcessVersion(adjustments.process),\n };\n}\n\n/** The default name of an annotation layer. */\nexport function annotationLayerName(annotation: Annotation): string {\n switch (annotation.type) {\n case 'pen':\n return 'Pen';\n case 'rect':\n return 'Rectangle';\n case 'circle':\n return 'Ellipse';\n case 'arrow':\n return 'Arrow';\n case 'line':\n return 'Line';\n case 'text':\n return annotation.text ? annotation.text.slice(0, 32) : 'Text';\n case 'image':\n return 'Image';\n }\n}\n\nexport function annotationLayerFor(annotation: Annotation): AnnotationLayer {\n return {\n type: 'annotation',\n id: annotation.id,\n name: annotationLayerName(annotation),\n visible: true,\n opacity: 1,\n blend: 'normal',\n annotation,\n };\n}\n\nexport function adjustmentNodeFromLegacy(layer: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const node = {\n type: 'adjustment',\n id: layer.id,\n name: layer.name ?? '',\n visible: layer.enabled !== false,\n opacity: layer.opacity ?? 1,\n blend: layer.blend ?? 'normal',\n kind: layer.kind,\n params: JSON.parse(JSON.stringify(layer.params)),\n } as AdjustmentLayerNode;\n if (layer.mask) {\n const mask = rasterMaskFromLegacy(layer.mask, width, height);\n if (mask) node.mask = mask;\n }\n return node;\n}\n\n/** The Background layer of an opened image (its pixels are the document source). */\nexport function backgroundLayer(filters: LayerFilter[] = [], mask?: RasterMask): PixelLayer {\n const layer: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n source: 'document',\n filters,\n };\n if (mask) layer.mask = mask;\n return layer;\n}\n\n/** A parametric depth mask on a layer. */\nexport function depthRasterMask(source: DepthMaskOptions): RasterMask {\n const out: RasterMask = {\n surface: emptySurface(1, 255),\n defaultValue: 255,\n source: { ...source, depthRange: [...source.depthRange] as [number, number] },\n };\n if (!source.enabled) out.enabled = false;\n return out;\n}\n\n/** Lifts a v1 / v2 state into a layer stack (see the module comment). */\nexport function layersFromLegacy(parts: Partial<LegacyParts>, width: number, height: number): LayerStack {\n // As `normalizeStoredAdjustments`: stored values without `process` keep the legacy rendering.\n const stored = parts.adjustments;\n const adjustments = (\n stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES }\n ) as ImageAdjustments;\n const adjLayers = normalizeAdjustmentLayers(adjustments.layers);\n const filters: LayerFilter[] = [developFromAdjustments(adjustments)];\n const blur = typeof adjustments.blur === 'number' && adjustments.blur > 0 ? adjustments.blur : 0;\n let legacyOutputBlur: number | undefined;\n if (blur > 0) {\n if (adjLayers.length) legacyOutputBlur = blur;\n else filters.push({ id: BLUR_FILTER_ID, kind: 'blur', enabled: true, radius: blur });\n }\n const depth = parts.depthMask;\n const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : undefined;\n const layers: ImageLayer[] = [backgroundLayer(filters, mask)];\n for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));\n for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));\n const out: LayerStack = { layers, outputOpacity: typeof adjustments.opacity === 'number' ? adjustments.opacity : 1 };\n if (legacyOutputBlur !== undefined) out.legacyOutputBlur = legacyOutputBlur;\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Layers → legacy views\n// ---------------------------------------------------------------------------\n\n/** The old `AdjustmentLayer` shape of a node, keys in the order `normalizeAdjustmentLayer` writes. */\nexport function legacyAdjustmentLayer(node: AdjustmentLayerNode, width: number, height: number): AdjustmentLayer {\n const out: Record<string, unknown> = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n };\n if (node.name) out.name = node.name;\n if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);\n return out as unknown as AdjustmentLayer;\n}\n\n/** The pixel layer whose develop filter the legacy API edits: the active one, else the Background. */\nexport function developTarget(layers: readonly ImageLayer[], activeLayerId: string | null): PixelLayer | null {\n const pixels: PixelLayer[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'pixel') pixels.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (activeLayerId) {\n const active = pixels.find((l) => l.id === activeLayerId);\n if (active) return active;\n }\n return pixels.find((l) => l.source === 'document') ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;\n}\n\nexport interface LegacyViewResult extends LegacyParts {\n /**\n * True when the stack has exactly the shape a v1 / v2 document lifts to (an unpainted,\n * unmoved Background with develop / blur filters and an optional depth mask, then adjustment\n * layers, then plain annotation layers), so it can be written as v1 / v2.\n */\n exact: boolean;\n}\n\nconst isPlain = (l: ImageLayer) =>\n l.visible && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask && !l.locks && !l.effects;\n\n/** Reads the stack back as the old document-level parts (see `LegacyViewResult`). */\nexport function legacyView(stack: LayerStack, width: number, height: number, activeLayerId: string | null = null): LegacyViewResult {\n const { layers } = stack;\n const target = developTarget(layers, activeLayerId);\n const develop = target ? developFilter(target) : undefined;\n const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : undefined) ?? 0;\n const adjustments: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS_VALUES,\n ...(develop\n ? {\n brightness: develop.brightness,\n contrast: develop.contrast,\n saturation: develop.saturation,\n exposure: develop.exposure,\n temperature: develop.temperature,\n tint: develop.tint ?? 0,\n }\n : {}),\n blur,\n opacity: stack.outputOpacity,\n process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process,\n };\n const adjNodes: AdjustmentLayerNode[] = [];\n const annotations: Annotation[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'adjustment') adjNodes.push(l);\n else if (l.type === 'annotation') annotations.push(l.annotation);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));\n const depthSource = target?.mask?.source;\n const depthMask: DepthMaskOptions = depthSource\n ? { ...depthSource, depthRange: [...depthSource.depthRange] as [number, number] }\n : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] as [number, number] };\n return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };\n}\n\n/** See `LegacyViewResult.exact`. */\nexport function isLegacyShape(stack: LayerStack): boolean {\n const { layers } = stack;\n const bg = layers[0];\n if (!bg || bg.type !== 'pixel' || bg.source !== 'document') return false;\n if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;\n if (!bg.visible || bg.opacity !== 1 || bg.blend !== 'normal' || bg.clipped || bg.locks || bg.effects) return false;\n if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;\n const filters = bg.filters ?? [];\n if (filters.length < 1 || filters.length > 2) return false;\n const [develop, blur] = filters;\n if (develop.kind !== 'develop' || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;\n if (blur && (blur.kind !== 'blur' || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;\n if (bg.mask) {\n const m = bg.mask;\n if (!m.source || m.density !== undefined || m.feather !== undefined) return false;\n if ((m.enabled !== false) !== m.source.enabled) return false;\n }\n let i = 1;\n let adjustments = 0;\n for (; i < layers.length && layers[i].type === 'adjustment'; i++) {\n const l = layers[i] as AdjustmentLayerNode;\n if (l.clipped || l.locks || l.mask?.source || l.effects) return false;\n adjustments++;\n }\n for (; i < layers.length; i++) {\n const l = layers[i];\n if (l.type !== 'annotation' || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;\n }\n if (blur && adjustments) return false;\n if (stack.legacyOutputBlur !== undefined && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;\n return true;\n}\n","/**\n * Pure helpers between the compositor and the renderer: the output stage (projection to the\n * cropped, rotated frame, output opacity and the migrated blur), the routing rule that keeps\n * old-shape documents on the exact legacy path where document-space compositing cannot match it,\n * and per-tile histograms. No DOM.\n */\n\nimport { applyBoxBlur } from '../filters';\nimport { normalizeAngle, unprojectCanvasPoint, getRotatedSize, type ImageProjection } from '../geometry';\nimport { HISTOGRAM_BINS, type ImageHistogram } from '../histogram';\nimport type { CropBox, ImageTransform } from '../types';\nimport { blurFilter, type AnnotationLayer, type ImageLayer, type PixelLayer } from './model';\nimport { hasResampledLegacyMask, isLegacyShape, type LayerStack } from './migrate';\nimport { LayerCompositor, type CompositorSources } from './compositor';\n\n/** True for 0, 90, 180 and 270 degrees. */\nexport function isQuarterTurn(rotate: number): boolean {\n return normalizeAngle(rotate) % 90 === 0;\n}\n\nfunction isIntegerRect(r: CropBox): boolean {\n return Number.isInteger(r.x) && Number.isInteger(r.y) && Number.isInteger(r.width) && Number.isInteger(r.height);\n}\n\n/** The projection that draws `region` into a `baseW × baseH` buffer at scale 1 (the renderer's base canvas). */\nexport function baseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/**\n * Projects `src` (RGBA covering `srcRect` in document space) into the base frame of `region`\n * under a quarter-turn `transform`: an exact pixel permutation (crop, flips, 90° steps), the\n * same mapping the canvas draws with. Returns the base buffer and its size.\n */\nexport function projectQuarterTurn(\n src: ArrayLike<number>,\n srcRect: CropBox,\n region: CropBox,\n transform: ImageTransform\n): { width: number; height: number; data: Uint8ClampedArray } {\n const rotated = getRotatedSize(region.width, region.height, transform);\n const w = Math.max(1, Math.round(rotated.width));\n const h = Math.max(1, Math.round(rotated.height));\n const out = new Uint8ClampedArray(w * h * 4);\n const p = baseProjection(region, transform, w, h);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, p);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, p);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, p);\n const ux = Math.round(ox.x - o.x);\n const vx = Math.round(ox.y - o.y);\n const uy = Math.round(oy.x - o.x);\n const vy = Math.round(oy.y - o.y);\n const u0 = Math.floor(o.x) - srcRect.x;\n const v0 = Math.floor(o.y) - srcRect.y;\n const sw = srcRect.width;\n const sh = srcRect.height;\n for (let y = 0; y < h; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n for (let x = 0; x < w; x++, u += ux, v += vx) {\n if (u < 0 || v < 0 || u >= sw || v >= sh) continue;\n const s = (v * sw + u) * 4;\n const d = (y * w + x) * 4;\n out[d] = src[s];\n out[d + 1] = src[s + 1];\n out[d + 2] = src[s + 2];\n out[d + 3] = src[s + 3];\n }\n }\n return { width: w, height: h, data: out };\n}\n\n/** Output opacity as the old base adjustments applied it (`clamp(a · opacity)`), then the migrated blur. */\nexport function applyOutputStage(img: { width: number; height: number; data: Uint8ClampedArray }, opacity: number, legacyOutputBlur?: number): void {\n if (opacity < 1) {\n const d = img.data;\n for (let i = 3; i < d.length; i += 4) d[i] = d[i] * opacity;\n }\n if (legacyOutputBlur && legacyOutputBlur > 0) applyBoxBlur(img, Math.min(Math.round(legacyOutputBlur), 40));\n}\n\nexport interface RouteInput {\n stack: LayerStack;\n region: CropBox;\n canvas: { width: number; height: number };\n transform: ImageTransform;\n /** Whether the source image is fully opaque. */\n sourceOpaque: boolean;\n}\n\n/**\n * Whether a document must render through the legacy base path to look exactly as before. Only\n * documents that still have the v1 / v2 shape qualify, and only where compositing in document\n * space cannot reproduce the old order of operations:\n *\n * - a rotation that is not a quarter turn, or a crop with fractional coordinates (the old path\n * resampled before adjusting);\n * - an enabled depth mask (applied after the blur and resampled with the canvas);\n * - output opacity below 1 (it rounded alpha before the depth mask);\n * - a Background blur filter with a crop or a 90° / 270° turn (the old blur clamped at the crop\n * edge and ran on the rotated buffer);\n * - a migrated output blur over a source with transparency (canvas premultiplication would round\n * colours before blurring);\n * - an adjustment-layer mask stored at another size than the canvas (it was resampled through\n * the projection on every render).\n */\nexport function needsLegacyBase(input: RouteInput): boolean {\n const { stack, region, canvas, transform } = input;\n if (!isLegacyShape(stack)) return false;\n const bg = stack.layers[0] as PixelLayer;\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) return true;\n if (bg.mask?.source?.enabled && bg.mask.source.maskSource) return true;\n if (stack.outputOpacity < 1) return true;\n const blur = blurFilter(bg);\n if (blur && blur.enabled && blur.radius > 0) {\n const full = region.x === 0 && region.y === 0 && region.width === canvas.width && region.height === canvas.height;\n const angle = normalizeAngle(transform.rotate);\n if (!full || angle === 90 || angle === 270) return true;\n }\n if ((stack.legacyOutputBlur ?? 0) > 0 && !input.sourceOpaque) return true;\n if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;\n return false;\n}\n\nconst isPlainAnnotation = (l: ImageLayer): l is AnnotationLayer =>\n l.type === 'annotation' && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask;\n\n/**\n * Splits the stack into what the compositor rasterizes and the annotation layers at the top that\n * draw as crisp vectors over the result (as annotations always did): the trailing run of plain\n * top-level annotation layers (hidden ones are skipped).\n */\nexport function splitOverlayAnnotations(layers: readonly ImageLayer[]): { composite: ImageLayer[]; overlay: AnnotationLayer[] } {\n let i = layers.length;\n while (i > 0) {\n const l = layers[i - 1];\n if (l.type === 'annotation' && (!l.visible || isPlainAnnotation(l))) i--;\n else break;\n }\n return {\n composite: layers.slice(0, i) as ImageLayer[],\n overlay: (layers.slice(i) as AnnotationLayer[]).filter((l) => l.visible),\n };\n}\n\n/** The integer rectangle covering `region`. */\nexport function coveringRect(region: CropBox): CropBox {\n const x = Math.floor(region.x);\n const y = Math.floor(region.y);\n return { x, y, width: Math.ceil(region.x + region.width) - x, height: Math.ceil(region.y + region.height) - y };\n}\n\n/**\n * The base image of a layered document without a canvas: the stack composited over `region`\n * (integer), projected under a quarter-turn `transform`, then output opacity and the migrated\n * blur. Annotation layers need a canvas and are skipped unless `sources` can rasterize them.\n * This is what the renderer's base canvas holds for such documents.\n */\nexport function renderLayeredBase(\n stack: LayerStack,\n region: CropBox,\n transform: ImageTransform,\n sources: CompositorSources,\n compositor = new LayerCompositor()\n): { width: number; height: number; data: Uint8ClampedArray } {\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) {\n throw new Error('renderLayeredBase needs an integer region and a quarter-turn rotation.');\n }\n const composite = compositor.composite(stack.layers, region, sources);\n const out = projectQuarterTurn(composite, region, region, transform);\n applyOutputStage(out, stack.outputOpacity, stack.legacyOutputBlur);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Histograms\n// ---------------------------------------------------------------------------\n\n/** An empty histogram. */\nexport function emptyHistogram(): ImageHistogram {\n return {\n red: new Uint32Array(HISTOGRAM_BINS),\n green: new Uint32Array(HISTOGRAM_BINS),\n blue: new Uint32Array(HISTOGRAM_BINS),\n luminance: new Uint32Array(HISTOGRAM_BINS),\n samples: 0,\n transparent: 0,\n shadows: [0, 0, 0],\n highlights: [0, 0, 0],\n max: 0,\n };\n}\n\n/** Adds the pixels of `rect` (in tile-local coordinates of a `stride`-wide RGBA buffer) to `h`. */\nexport function addToHistogram(h: ImageHistogram, data: ArrayLike<number>, stride: number, rect: CropBox): void {\n for (let y = rect.y; y < rect.y + rect.height; y++) {\n let i = (y * stride + rect.x) * 4;\n for (let x = 0; x < rect.width; x++, i += 4) {\n if (!(data[i + 3] > 0)) {\n h.transparent++;\n continue;\n }\n h.samples++;\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n h.red[r]++;\n h.green[g]++;\n h.blue[b]++;\n h.luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) h.shadows[0]++;\n if (g === 0) h.shadows[1]++;\n if (b === 0) h.shadows[2]++;\n if (r === 255) h.highlights[0]++;\n if (g === 255) h.highlights[1]++;\n if (b === 255) h.highlights[2]++;\n }\n }\n}\n\n/** Sums histograms (and recomputes `max`). */\nexport function sumHistograms(parts: Iterable<ImageHistogram>): ImageHistogram {\n const out = emptyHistogram();\n for (const p of parts) {\n for (let i = 0; i < HISTOGRAM_BINS; i++) {\n out.red[i] += p.red[i];\n out.green[i] += p.green[i];\n out.blue[i] += p.blue[i];\n out.luminance[i] += p.luminance[i];\n }\n out.samples += p.samples;\n out.transparent += p.transparent;\n for (let c = 0; c < 3; c++) {\n out.shadows[c] += p.shadows[c];\n out.highlights[c] += p.highlights[c];\n }\n }\n let max = 0;\n for (let i = 0; i < HISTOGRAM_BINS; i++) max = Math.max(max, out.red[i], out.green[i], out.blue[i], out.luminance[i]);\n out.max = max;\n return out;\n}\n","import type {\n Annotation,\n CropBox,\n ImagePoint,\n ExportOptions,\n ImageEditorFit,\n ImageEditorState,\n ImageTransform,\n PenAnnotation,\n RectAnnotation,\n CircleAnnotation,\n ArrowAnnotation,\n LineAnnotation,\n TextAnnotation,\n ImageAnnotation,\n} from './types';\nimport { applyAdjustments, applyDepthMask } from './filters';\nimport { adjustmentLayersKey } from './adjustment-layers';\nimport { canvasCompositeOperation, blendImageData, normalizeBlendMode } from './blend';\nimport { computeHistogram, type ImageHistogram } from './histogram';\nimport { normalizeProcessVersion } from './color';\nimport { buildLayerMaskPlanes } from './selection/layer-mask';\nimport { selectionOutline, quickMaskPixels } from './selection/overlay';\nimport type { SelectionMask } from './selection/mask';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport { flattenLayers, type ImageLayer, type PixelLayer } from './layers/model';\nimport {\n addToHistogram,\n applyOutputStage,\n baseProjection,\n coveringRect,\n emptyHistogram,\n needsLegacyBase,\n splitOverlayAnnotations,\n sumHistograms,\n} from './layers/render';\nimport { TILE, tileKey, type PixelSurface, type TileKey } from '../raster/surface';\nimport type { GlobalLight } from '../raster/effects/model';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { isLegacyShape as isLegacyStack } from './layers/migrate';\nimport {\n applyImageProjectionToContext,\n projectImagePoint,\n clampCropToImage,\n getAnnotationBounds,\n getRotatedSize,\n getViewportMetrics,\n getViewportProjection,\n normalizeAngle,\n type ImageProjection,\n type ViewportMetrics,\n} from './geometry';\n\nexport interface RenderOptions {\n viewportWidth?: number;\n viewportHeight?: number;\n showCropOverlay?: boolean;\n showSelectionOverlay?: boolean;\n interactive?: boolean;\n draftAnnotation?: Annotation | null;\n draftCrop?: CropBox | null;\n /** Viewport fit mode: 'contain' scales to full viewport bounds; 'margin' scales to 85% with 1.0 cap (default). */\n fit?: ImageEditorFit;\n /**\n * How the active selection is shown: marching ants (default when there is a selection),\n * a Quick Mask tint over the unselected area (default when `state.quickMask`), or not at all.\n */\n selectionDisplay?: 'ants' | 'quickMask' | 'none';\n /** Dash offset of the marching ants in screen pixels (animate it to make them march). */\n antsOffset?: number;\n /** A selection being drawn (e.g. a lasso), shown as a dashed outline in natural pixels. */\n draftSelectionPath?: ImagePoint[] | null;\n /** Document rectangles that are the only change since the last render (strokes); recomposites only there. */\n dirty?: CropBox[];\n /** The layer being edited: tiles keep a checkpoint below it so strokes and drags on it start there. */\n checkpointLayerId?: string | null;\n /** The paint tools' cursor: a circle of `diameter` px at (`x`, `y`) in natural pixels. */\n brushCursor?: { x: number; y: number; diameter: number } | null;\n /** Free Transform's frame, handles and reference point in natural pixels (`FreeTransformTool.overlay()`). */\n freeTransform?: {\n outline: ImagePoint[];\n handles: ImagePoint[];\n reference: ImagePoint;\n /** Warp mode: the mesh's grid lines as cubics, its anchors, and handles with their anchors. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n } | null;\n}\n\n/**\n * Image Cache to avoid refetching HTMLImageElement objects repeatedly during re-renders.\n */\nclass ImageCache {\n private cache = new Map<string, HTMLImageElement>();\n\n public async get(src: string): Promise<HTMLImageElement> {\n const existing = this.cache.get(src);\n if (existing && existing.complete) {\n return existing;\n }\n\n return new Promise((resolve, reject) => {\n if (typeof window === 'undefined' || typeof Image === 'undefined') {\n reject(new Error('Image loading is only supported in browser/DOM environments'));\n return;\n }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n this.cache.set(src, img);\n resolve(img);\n };\n img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));\n img.src = src;\n\n // In JSDOM or test environments where Image resource loading is inactive, resolve promptly\n if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {\n setTimeout(() => {\n try {\n Object.defineProperty(img, 'naturalWidth', { value: 800, configurable: true });\n Object.defineProperty(img, 'naturalHeight', { value: 600, configurable: true });\n Object.defineProperty(img, 'complete', { value: true, configurable: true });\n } catch {\n // Ignore definition errors in restrictive environments\n }\n this.cache.set(src, img);\n resolve(img);\n }, 10);\n }\n });\n }\n\n public set(src: string, img: HTMLImageElement): void {\n this.cache.set(src, img);\n }\n\n /** The loaded image for `src`, or `undefined` while it is not loaded yet. */\n public peek(src: string): HTMLImageElement | undefined {\n const img = this.cache.get(src);\n return img && img.complete ? img : undefined;\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\nexport const globalImageCache = new ImageCache();\n\n/**\n * Renders an image with all its transformations, filters, depth masks, and annotations\n * to a target HTMLCanvasElement.\n */\nexport class ImageEditorRenderer {\n public imageCache = globalImageCache;\n private cachedBaseCanvas: HTMLCanvasElement | null = null;\n private cachedBaseKey = '';\n /**\n * Histogram of the last viewport base render (after adjustments and layers, before\n * annotations); `null` until a render with pixel access has happened.\n */\n public previewHistogram: ImageHistogram | null = null;\n /** Called whenever the viewport base image is re-rendered with the new histogram. */\n public onPreviewHistogram?: (histogram: ImageHistogram | null) => void;\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 0;\n /**\n * The viewport's compositor. It only ever composites the viewport's stack, so its tile cache can\n * trust dirty rectangles; exports use `exportCompositor`.\n */\n public compositor = new LayerCompositor();\n private exportCompositor = new LayerCompositor({ maxCacheBytes: 64 * 1024 * 1024 });\n /** How the host resolves layer pixels (set by the controller). */\n public layerSources?: LayerSourcesHost;\n /**\n * Debug / test switch: `'legacy'` forces the old base path for documents that still have the\n * v1 / v2 shape, `'layers'` forces the compositor. Default: chosen per document.\n */\n public baseRoute: 'auto' | 'legacy' | 'layers' = 'auto';\n /** Which path the last base render took. */\n public lastBaseRoute: 'legacy' | 'layers' | null = null;\n private comp: CompositeCanvas | null = null;\n private depthMaps = new Map<string, { width: number; height: number; data: Uint8ClampedArray }>();\n private assetVersion = 0;\n private scratch: HTMLCanvasElement | null = null;\n private cachedPostCanvas: HTMLCanvasElement | null = null;\n private cachedPostKey = '';\n private cachedPostHistogram: ImageHistogram | null = null;\n\n /**\n * Helper to create a canvas element safely in browser/JSDOM.\n */\n public createCanvas(width: number, height: number): HTMLCanvasElement {\n if (typeof document !== 'undefined' && document.createElement) {\n const c = document.createElement('canvas');\n c.width = Math.max(1, Math.round(width));\n c.height = Math.max(1, Math.round(height));\n return c;\n }\n // Fallback for non-DOM / headless\n return {\n width: Math.max(1, Math.round(width)),\n height: Math.max(1, Math.round(height)),\n getContext: () => null,\n toDataURL: () => '',\n toBlob: () => {},\n } as unknown as HTMLCanvasElement;\n }\n\n /**\n * Renders the editor state onto a canvas (viewport or preview).\n */\n public async renderToCanvas(\n targetCanvas: HTMLCanvasElement,\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: RenderOptions = {}\n ): Promise<void> {\n const ctx = targetCanvas.getContext('2d');\n if (!ctx) return;\n\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const fit = options.fit ?? state.fit ?? 'margin';\n // Geometry helpers read `imageDimensions`; make sure they see the real source size so the\n // viewport drawn here and the hit-testing done by the UI share one projection.\n const geoState: ImageEditorState = {\n ...state,\n fit,\n imageDimensions:\n state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight\n ? state.imageDimensions\n : { width: naturalWidth, height: naturalHeight },\n };\n\n const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;\n\n // Viewport dimensions\n const vpWidth = options.viewportWidth || targetCanvas.width;\n const vpHeight = options.viewportHeight || targetCanvas.height;\n\n targetCanvas.width = vpWidth;\n targetCanvas.height = vpHeight;\n\n ctx.clearRect(0, 0, vpWidth, vpHeight);\n\n // If in compare mode, render original image directly and exit\n if (isOriginalCompared) {\n this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan, fit);\n return;\n }\n\n // When actively in crop tool or overlay requested, show full image so user can adjust the crop box\n const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === 'crop');\n const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);\n const { region } = projection;\n const rotated = getRotatedSize(region.width, region.height, transform);\n\n const route = this.chooseRoute(state, region, naturalWidth, naturalHeight);\n let overlay: Annotation[] = annotations;\n if (route === 'legacy') {\n // 1. Offscreen canvas for Base Image + Transform (Flip/Rotate) + Crop + Adjustments + Depth mask\n const baseKey = [\n this.getSourceId(img),\n state.sourceUrl,\n `${naturalWidth}x${naturalHeight}`,\n `${region.x},${region.y},${region.width},${region.height}`,\n `${projection.angle},${transform.flipH},${transform.flipV}`,\n `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,\n adjustmentLayersKey(adjustments.layers),\n `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`,\n ].join('|');\n\n let baseCanvas = this.cachedBaseCanvas;\n if (!baseCanvas || this.cachedBaseKey !== baseKey) {\n let histogram: ImageHistogram | null = null;\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {\n histogram = computeHistogram(pixels);\n });\n this.cachedBaseCanvas = baseCanvas;\n this.cachedBaseKey = baseKey;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n // 2. Draw the base canvas into the viewport (centred, fitted, zoomed, panned)\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n } else {\n // 1–2. The layer stack, composited per tile in document space, drawn through the projection.\n const split = splitOverlayAnnotations(state.layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const comp = this.updateComposite(split.composite, region, naturalWidth, naturalHeight, options);\n const base = this.layeredBase(comp, state, region, transform);\n if (base) {\n // Drawn exactly as the legacy base canvas is, so both paths look the same at any zoom.\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(base.canvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n this.publishHistogram(base.histogram ?? comp?.histogram() ?? null);\n }\n }\n\n // 3. Annotations live in natural image space: draw them through the same projection,\n // clipped to the visible (cropped) region.\n if (overlay.length > 0 || options.draftAnnotation) {\n ctx.save();\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {\n this.drawAnnotationSelection(ctx, ann);\n }\n }\n // A selected annotation drawn inside the composite still gets its handles.\n if (options.showSelectionOverlay && selectedAnnotationId && !overlay.some((a) => a.id === selectedAnnotationId)) {\n const sel = annotations.find((a) => a.id === selectedAnnotationId);\n if (sel) this.drawAnnotationSelection(ctx, sel);\n }\n\n // Render active live draft annotation\n if (options.draftAnnotation) {\n await this.drawAnnotation(ctx, options.draftAnnotation);\n }\n\n ctx.restore();\n }\n\n // 4. Selection: marching ants or Quick Mask, through the same projection.\n const display = options.selectionDisplay ?? (state.quickMask ? 'quickMask' : 'ants');\n if ((state.selection?.bounds && display !== 'none') || options.draftSelectionPath?.length) {\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (state.selection?.bounds && display === 'quickMask') this.drawQuickMask(ctx, state.selection);\n else if (state.selection?.bounds && display === 'ants') this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);\n if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);\n } catch {\n // Overlays are best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Brush cursor: the tip outline, one screen pixel wide, white over black.\n if (options.brushCursor) {\n const b = options.brushCursor;\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n const px = 1 / (projection.scale || 1);\n ctx.beginPath();\n ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);\n ctx.lineWidth = 2 * px;\n ctx.strokeStyle = 'rgba(0,0,0,0.6)';\n ctx.stroke();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Free Transform: the frame, square handles and the reference point, a fixed size on screen.\n if (options.freeTransform) {\n const t = options.freeTransform;\n ctx.save();\n try {\n const scr = (p: ImagePoint) => projectImagePoint(p, projection);\n ctx.beginPath();\n if (t.warp) {\n for (const c of t.warp.curves) {\n const [a, b, d, e] = c.map(scr);\n ctx.moveTo(a.x, a.y);\n ctx.bezierCurveTo(b.x, b.y, d.x, d.y, e.x, e.y);\n }\n for (const h of t.warp.handles) {\n const a = scr(h.anchor);\n const q = scr(h.point);\n ctx.moveTo(a.x, a.y);\n ctx.lineTo(q.x, q.y);\n }\n } else {\n const outline = t.outline.map(scr);\n outline.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));\n ctx.closePath();\n }\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n for (const h of [...t.handles, ...(t.warp?.anchors ?? [])].map(scr)) {\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#1e293b';\n ctx.fillRect(h.x - 4, h.y - 4, 8, 8);\n ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);\n }\n for (const h of (t.warp?.handles ?? []).map((w) => scr(w.point))) {\n ctx.beginPath();\n ctx.arc(h.x, h.y, 3, 0, Math.PI * 2);\n ctx.fillStyle = '#ffffff';\n ctx.fill();\n ctx.strokeStyle = '#1e293b';\n ctx.stroke();\n }\n const r = scr(t.reference);\n ctx.beginPath();\n ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);\n ctx.moveTo(r.x - 8, r.y);\n ctx.lineTo(r.x + 8, r.y);\n ctx.moveTo(r.x, r.y - 8);\n ctx.lineTo(r.x, r.y + 8);\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // 5. Draw Crop Grid Overlay if crop overlay is requested\n if (isActivelyCropping) {\n const activeCrop =\n options.draftCrop !== undefined && options.draftCrop !== null\n ? options.draftCrop\n : (crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight });\n this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));\n }\n }\n\n /**\n * Draws `region` of the source image with rotation/flip applied, then pixel adjustments\n * and the depth mask (which receives the identical transform so it stays aligned).\n */\n private async renderBaseImage(\n img: CanvasImageSource,\n naturalWidth: number,\n naturalHeight: number,\n region: CropBox,\n transform: ImageTransform,\n state: Pick<ImageEditorState, 'adjustments' | 'depthMask'>,\n onPixels?: (pixels: ImageData) => void\n ): Promise<HTMLCanvasElement> {\n const { adjustments, depthMask } = state;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const baseCanvas = this.createCanvas(baseW, baseH);\n const baseCtx = baseCanvas.getContext('2d');\n if (!baseCtx) return baseCanvas;\n\n const projection = getBaseProjection(region, transform, baseW, baseH);\n\n baseCtx.save();\n applyImageProjectionToContext(baseCtx, projection);\n baseCtx.drawImage(\n img,\n region.x, region.y, region.width, region.height,\n region.x, region.y, region.width, region.height\n );\n baseCtx.restore();\n\n const imageData = baseCtx.getImageData(0, 0, baseW, baseH);\n if (imageData) {\n // Layer masks live in natural pixels: resample them through the same projection.\n const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);\n applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});\n\n if (depthMask.enabled && depthMask.maskSource) {\n try {\n const maskImg = await this.imageCache.get(depthMask.maskSource);\n const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);\n const sx = maskSize.width / naturalWidth;\n const sy = maskSize.height / naturalHeight;\n const maskCanvas = this.createCanvas(baseW, baseH);\n const maskCtx = maskCanvas.getContext('2d');\n if (maskCtx) {\n // The depth map covers the whole source image; scale it to natural pixels and\n // apply the exact same rotate/flip/crop projection as the image itself.\n maskCtx.save();\n applyImageProjectionToContext(maskCtx, projection);\n maskCtx.drawImage(\n maskImg,\n region.x * sx, region.y * sy, region.width * sx, region.height * sy,\n region.x, region.y, region.width, region.height\n );\n maskCtx.restore();\n const maskData = maskCtx.getImageData(0, 0, baseW, baseH);\n if (maskData) {\n applyDepthMask(imageData, maskData, depthMask);\n }\n }\n } catch {\n // Mask failed to load; continue without crashing\n }\n }\n\n onPixels?.(imageData);\n baseCtx.putImageData(imageData, 0, 0);\n }\n\n return baseCanvas;\n }\n\n // --- Layered documents -------------------------------------------------------\n\n /** Which base path renders `state` (see `needsLegacyBase`). */\n private chooseRoute(state: ImageEditorState, region: CropBox, naturalWidth: number, naturalHeight: number): 'legacy' | 'layers' {\n let route: 'legacy' | 'layers';\n if (!state.layers?.length) route = 'legacy';\n else if (this.baseRoute === 'layers') route = 'layers';\n else {\n const stack = { layers: state.layers, outputOpacity: state.outputOpacity ?? 1, legacyOutputBlur: state.legacyOutputBlur };\n const shaped = this.baseRoute === 'legacy' && isLegacyStack(stack);\n route =\n shaped ||\n needsLegacyBase({\n stack,\n region,\n canvas: { width: naturalWidth, height: naturalHeight },\n transform: state.transform,\n sourceOpaque: this.layerSources?.sourceIsOpaque() ?? true,\n })\n ? 'legacy'\n : 'layers';\n }\n this.lastBaseRoute = route;\n return route;\n }\n\n /** Compositor sources for a `width × height` canvas: layer pixels, depth maps and annotation rasterizing. */\n public compositorSources(width: number, height: number): CompositorSources {\n const host = this.layerSources;\n return {\n width,\n height,\n surfaceOf: host ? (l: PixelLayer) => host.surfaceOf(l) : undefined,\n depthMap: (source) => {\n if (!source.enabled || !source.maskSource) return null;\n return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;\n },\n rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),\n version: `${host?.version() ?? ''}:${this.assetVersion}`,\n globalLight: host?.globalLight?.(),\n pattern: host?.pattern ? (id) => host.pattern!(id) : undefined,\n };\n }\n\n /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */\n /** Loads depth maps and annotation images the compositor reads synchronously. */\n public async prepareLayerAssets(layers: readonly ImageLayer[], width: number, height: number): Promise<void> {\n for (const l of flattenLayers(layers)) {\n const source = l.mask?.source;\n if (source?.enabled && source.maskSource) {\n const key = `${source.maskSource}|${width}x${height}`;\n if (this.depthMaps.has(key)) continue;\n try {\n const maskImg = await this.imageCache.get(source.maskSource);\n const canvas = this.createCanvas(width, height);\n const mctx = canvas.getContext('2d');\n const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;\n if (data) {\n this.depthMaps.set(key, { width, height, data: data.data });\n this.assetVersion++;\n }\n } catch {\n // A depth map that fails to load reveals the layer, as before.\n }\n }\n if (l.type === 'annotation' && l.annotation.type === 'image' && !this.imageCache.peek(l.annotation.src)) {\n try {\n await this.imageCache.get(l.annotation.src);\n this.assetVersion++;\n } catch {\n // Skipped, as a watermark that fails to load always was.\n }\n }\n }\n }\n\n /** Whether annotation layers can be rasterized here (a 2D canvas with pixel access). */\n public canRasterize(): boolean {\n const ctx = this.createCanvas(1, 1).getContext('2d');\n return Boolean(ctx && typeof ctx.getImageData === 'function');\n }\n\n /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */\n private rasterizeAnnotation(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null {\n if (!this.scratch) this.scratch = this.createCanvas(rect.width, rect.height);\n const canvas = this.scratch;\n if (canvas.width !== rect.width || canvas.height !== rect.height) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n try {\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n ctx.setTransform(1, 0, 0, 1, -rect.x, -rect.y);\n const img = annotation.type === 'image' ? this.imageCache.peek(annotation.src) ?? null : null;\n this.drawAnnotationShape(ctx, annotation, img);\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n return new Uint8ClampedArray(ctx.getImageData(0, 0, rect.width, rect.height).data);\n } catch {\n return null;\n }\n }\n\n private putTile(ctx: CanvasRenderingContext2D, data: Uint8ClampedArray, tx: number, ty: number, rect: CropBox): void {\n const ox = tx * TILE - rect.x;\n const oy = ty * TILE - rect.y;\n const x0 = Math.max(0, -ox);\n const y0 = Math.max(0, -oy);\n const x1 = Math.min(TILE, rect.width - ox);\n const y1 = Math.min(TILE, rect.height - oy);\n if (x1 <= x0 || y1 <= y0) return;\n const img = ctx.createImageData(TILE, TILE);\n img.data.set(data);\n ctx.putImageData(img, ox, oy, x0, y0, x1 - x0, y1 - y0);\n }\n\n /**\n * Brings the viewport's composite canvas (document space, covering `region`) up to date,\n * uploading only the tiles that changed since the last render.\n */\n private updateComposite(\n layers: readonly ImageLayer[],\n region: CropBox,\n width: number,\n height: number,\n options: Pick<RenderOptions, 'dirty' | 'checkpointLayerId'> = {}\n ): CompositeCanvas | null {\n const rect = coveringRect(region);\n let comp = this.comp;\n let fresh = false;\n if (!comp || comp.rect.x !== rect.x || comp.rect.y !== rect.y || comp.rect.width !== rect.width || comp.rect.height !== rect.height) {\n const canvas = this.createCanvas(rect.width, rect.height);\n comp = this.comp = new CompositeCanvas(canvas, rect);\n fresh = true;\n }\n const ctx = comp.canvas.getContext('2d');\n if (!ctx || typeof ctx.createImageData !== 'function') return null;\n let changed = false;\n this.compositor.compositeTiles(layers, rect, this.compositorSources(width, height), (tx, ty, data) => {\n const key = tileKey(tx, ty);\n if (comp!.tiles.get(key) === data) return;\n comp!.tiles.set(key, data);\n comp!.histograms.delete(key);\n this.putTile(ctx, data, tx, ty, rect);\n changed = true;\n }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? undefined : options.dirty });\n if (changed) comp.version++;\n comp.region = region;\n return comp;\n }\n\n /**\n * The base canvas of a layered document: the composite drawn through the base projection (crop,\n * rotation, flips at scale 1), then output opacity and the migrated blur, which run in the output\n * frame. Cached until the composite or those settings change.\n */\n private layeredBase(\n comp: CompositeCanvas | null,\n state: ImageEditorState,\n region: CropBox,\n transform: ImageTransform\n ): { canvas: HTMLCanvasElement; histogram: ImageHistogram | null } | null {\n if (!comp) return null;\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const key = `${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;\n if (this.cachedPostCanvas && this.cachedPostKey === key) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };\n const canvas =\n this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH\n ? this.cachedPostCanvas\n : this.createCanvas(baseW, baseH);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n ctx.clearRect(0, 0, baseW, baseH);\n ctx.save();\n applyImageProjectionToContext(ctx, baseProjection(region, transform, baseW, baseH));\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n ctx.drawImage(comp.canvas, comp.rect.x, comp.rect.y);\n ctx.restore();\n const histogram = this.applyPost(ctx, baseW, baseH, state);\n this.cachedPostCanvas = canvas;\n this.cachedPostKey = key;\n this.cachedPostHistogram = histogram;\n return { canvas, histogram };\n }\n\n /** Output opacity then the migrated blur, in place on a base canvas; returns its histogram. */\n private applyPost(ctx: CanvasRenderingContext2D, w: number, h: number, state: ImageEditorState): ImageHistogram | null {\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n if (!(opacity < 1) && !(blur > 0)) return null;\n const data = ctx.getImageData?.(0, 0, w, h);\n if (!data) return null;\n applyOutputStage(data, opacity, blur);\n ctx.putImageData(data, 0, 0);\n return computeHistogram(data);\n }\n\n private publishHistogram(histogram: ImageHistogram | null): void {\n if (histogram === this.previewHistogram) return;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n private quickMaskCache = new WeakMap<SelectionMask, HTMLCanvasElement>();\n private outlineCache = new WeakMap<SelectionMask, Float32Array>();\n\n /** Rubylith over the unselected area, drawn in natural image space. */\n private drawQuickMask(ctx: CanvasRenderingContext2D, selection: SelectionMask): void {\n let canvas = this.quickMaskCache.get(selection);\n if (!canvas) {\n canvas = this.createCanvas(selection.width, selection.height);\n const mctx = canvas.getContext('2d');\n if (!mctx || typeof mctx.createImageData !== 'function') return;\n const img = mctx.createImageData(selection.width, selection.height);\n img.data.set(quickMaskPixels(selection));\n mctx.putImageData(img, 0, 0);\n this.quickMaskCache.set(selection, canvas);\n }\n ctx.drawImage(canvas, 0, 0, selection.width, selection.height);\n }\n\n /** Marching ants: a white line under a black dashed one, one screen pixel wide. */\n private drawAnts(ctx: CanvasRenderingContext2D, selection: SelectionMask, scale: number, offset: number): void {\n let segs = this.outlineCache.get(selection);\n if (!segs) {\n segs = selectionOutline(selection);\n this.outlineCache.set(selection, segs);\n }\n if (!segs.length) return;\n const px = 1 / (scale || 1);\n ctx.beginPath();\n for (let i = 0; i < segs.length; i += 4) {\n ctx.moveTo(segs[i], segs[i + 1]);\n ctx.lineTo(segs[i + 2], segs[i + 3]);\n }\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') {\n ctx.setLineDash([4 * px, 4 * px]);\n ctx.lineDashOffset = -offset * px;\n }\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private drawDraftPath(ctx: CanvasRenderingContext2D, points: ImagePoint[], scale: number): void {\n const px = 1 / (scale || 1);\n ctx.beginPath();\n points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)));\n ctx.closePath();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([4 * px, 4 * px]);\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private getSourceId(img: CanvasImageSource): number {\n const key = img as unknown as object;\n let id = this.sourceIds.get(key);\n if (id === undefined) {\n id = ++this.nextSourceId;\n this.sourceIds.set(key, id);\n }\n return id;\n }\n\n /**\n * Draws an individual annotation onto the canvas.\n */\n public async drawAnnotation(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n const blend = normalizeBlendMode(ann.blend);\n if (blend === 'normal') {\n await this.drawAnnotationContent(ctx, ann);\n return;\n }\n const op = canvasCompositeOperation(blend);\n if (op) {\n ctx.save();\n ctx.globalCompositeOperation = op;\n await this.drawAnnotationContent(ctx, ann);\n ctx.restore();\n return;\n }\n if (!(await this.drawAnnotationBlended(ctx, ann))) {\n // No pixel access (e.g. a tainted or headless canvas): draw it normally.\n await this.drawAnnotationContent(ctx, ann);\n }\n }\n\n /**\n * Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /\n * vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):\n * draws the annotation on a same-size layer with the same transform, blends it into a copy of\n * the target's pixels with `blendImageData`, and copies the result back inside the current clip.\n */\n private async drawAnnotationBlended(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<boolean> {\n const canvas = ctx.canvas as HTMLCanvasElement | undefined;\n if (!canvas || typeof ctx.getTransform !== 'function' || typeof ctx.getImageData !== 'function') return false;\n const w = canvas.width;\n const h = canvas.height;\n const layer = this.createCanvas(w, h);\n const lctx = layer.getContext('2d');\n if (!lctx) return false;\n try {\n lctx.setTransform(ctx.getTransform());\n lctx.globalAlpha = ctx.globalAlpha;\n await this.drawAnnotationContent(lctx, ann);\n const backdrop = ctx.getImageData(0, 0, w, h);\n const source = lctx.getImageData(0, 0, w, h);\n if (!backdrop || !source) return false;\n blendImageData(backdrop, source, normalizeBlendMode(ann.blend));\n lctx.setTransform(1, 0, 0, 1, 0, 0);\n lctx.putImageData(backdrop, 0, 0);\n } catch {\n return false;\n }\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(layer, 0, 0);\n ctx.restore();\n return true;\n }\n\n private async drawAnnotationContent(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n let img: CanvasImageSource | null = null;\n if (ann.type === 'image') {\n try {\n img = await this.imageCache.get(ann.src);\n } catch {\n // Skip if watermark image failed to load\n }\n }\n this.drawAnnotationShape(ctx, ann, img);\n }\n\n /** Draws an annotation synchronously; an image annotation draws `img` (skipped when `null`). */\n public drawAnnotationShape(ctx: CanvasRenderingContext2D, ann: Annotation, img: CanvasImageSource | null): void {\n ctx.save();\n if (ann.opacity !== undefined) {\n ctx.globalAlpha = ann.opacity;\n }\n\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n const pen = ann as PenAnnotation;\n if (pen.points.length > 0) {\n ctx.beginPath();\n ctx.strokeStyle = pen.strokeColor;\n ctx.lineWidth = pen.strokeWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.moveTo(pen.points[0].x, pen.points[0].y);\n for (let i = 1; i < pen.points.length; i++) {\n ctx.lineTo(pen.points[i].x, pen.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n\n case 'rect': {\n const rect = ann as RectAnnotation;\n ctx.strokeStyle = rect.strokeColor;\n ctx.lineWidth = rect.strokeWidth;\n if (rect.fillColor) {\n ctx.fillStyle = rect.fillColor;\n ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n if (rect.strokeWidth > 0) {\n ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);\n }\n break;\n }\n\n case 'circle': {\n const circle = ann as CircleAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = circle.strokeColor;\n ctx.lineWidth = circle.strokeWidth;\n ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);\n if (circle.fillColor) {\n ctx.fillStyle = circle.fillColor;\n ctx.fill();\n }\n if (circle.strokeWidth > 0) {\n ctx.stroke();\n }\n break;\n }\n\n case 'arrow': {\n const arrow = ann as ArrowAnnotation;\n ctx.strokeStyle = arrow.strokeColor;\n ctx.fillStyle = arrow.strokeColor;\n ctx.lineWidth = arrow.strokeWidth;\n\n // Line body\n ctx.beginPath();\n ctx.moveTo(arrow.x, arrow.y);\n ctx.lineTo(arrow.endX, arrow.endY);\n ctx.stroke();\n\n // Arrow head\n const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);\n const headLen = Math.max(12, arrow.strokeWidth * 3);\n ctx.beginPath();\n ctx.moveTo(arrow.endX, arrow.endY);\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle - Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle - Math.PI / 6)\n );\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle + Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle + Math.PI / 6)\n );\n ctx.closePath();\n ctx.fill();\n break;\n }\n\n case 'line': {\n const line = ann as LineAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = line.strokeColor;\n ctx.lineWidth = line.strokeWidth;\n ctx.moveTo(line.x, line.y);\n ctx.lineTo(line.endX, line.endY);\n ctx.stroke();\n break;\n }\n\n case 'text': {\n const textAnn = ann as TextAnnotation;\n ctx.font = `${textAnn.fontWeight || 'normal'} ${textAnn.fontSize}px ${textAnn.fontFamily || 'sans-serif'}`;\n ctx.fillStyle = textAnn.color;\n ctx.textBaseline = 'top';\n ctx.fillText(textAnn.text, textAnn.x, textAnn.y);\n break;\n }\n\n case 'image': {\n const imgAnn = ann as ImageAnnotation;\n if (img) ctx.drawImage(img, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);\n break;\n }\n }\n\n ctx.restore();\n }\n\n /**\n * Draws selection box around active annotation.\n */\n private drawAnnotationSelection(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.strokeStyle = '#3b82f6'; // Tailwind blue-500\n ctx.lineWidth = 1.5;\n ctx.setLineDash([4, 4]);\n\n const bounds = getAnnotationBounds(ann);\n const pad = 4;\n ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);\n\n // Draw handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#3b82f6';\n ctx.setLineDash([]);\n const handleSize = 6;\n const corners = [\n { x: bounds.x - pad, y: bounds.y - pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y - pad },\n { x: bounds.x - pad, y: bounds.y + bounds.height + pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad },\n ];\n for (const c of corners) {\n ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */\n public getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n return getAnnotationBounds(ann);\n }\n\n /**\n * Draws a rule-of-thirds crop grid overlay. All rectangles come from\n * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.\n */\n private drawCropOverlay(ctx: CanvasRenderingContext2D, metrics: ViewportMetrics): void {\n const { imageRect, cropRect } = metrics;\n if (!cropRect) return;\n\n const imgL = imageRect.x;\n const imgT = imageRect.y;\n const imgR = imageRect.x + imageRect.width;\n const imgB = imageRect.y + imageRect.height;\n const startX = cropRect.x;\n const startY = cropRect.y;\n const cropBoxW = cropRect.width;\n const cropBoxH = cropRect.height;\n\n ctx.save();\n\n // Dark semi-transparent scrim outside crop box (within the image)\n ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n // Top\n ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));\n // Bottom\n ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));\n // Left\n ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);\n // Right\n ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);\n\n // Crop border\n ctx.strokeStyle = '#ffffff';\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);\n\n // Rule of thirds lines\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n // Vertical lines\n ctx.moveTo(startX + cropBoxW / 3, startY);\n ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);\n ctx.moveTo(startX + (cropBoxW * 2) / 3, startY);\n ctx.lineTo(startX + (cropBoxW * 2) / 3, startY + cropBoxH);\n // Horizontal lines\n ctx.moveTo(startX, startY + cropBoxH / 3);\n ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);\n ctx.moveTo(startX, startY + (cropBoxH * 2) / 3);\n ctx.lineTo(startX + cropBoxW, startY + (cropBoxH * 2) / 3);\n ctx.stroke();\n\n // Corner and edge handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#0284c7';\n ctx.lineWidth = 1.5;\n const handleSize = 10;\n const handles = [\n // Corners\n { x: startX, y: startY },\n { x: startX + cropBoxW, y: startY },\n { x: startX, y: startY + cropBoxH },\n { x: startX + cropBoxW, y: startY + cropBoxH },\n // Edges\n { x: startX + cropBoxW / 2, y: startY },\n { x: startX + cropBoxW / 2, y: startY + cropBoxH },\n { x: startX, y: startY + cropBoxH / 2 },\n { x: startX + cropBoxW, y: startY + cropBoxH / 2 },\n ];\n for (const h of handles) {\n ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n private drawFittedImage(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n imgW: number,\n imgH: number,\n vpW: number,\n vpH: number,\n zoom: number,\n pan: { x: number; y: number },\n fit: ImageEditorFit = 'margin'\n ): void {\n ctx.save();\n ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);\n ctx.scale(zoom, zoom);\n const fitScale =\n fit === 'contain'\n ? Math.min(vpW / imgW, vpH / imgH)\n : Math.min((vpW * 0.85) / imgW, (vpH * 0.85) / imgH, 1);\n const renderW = imgW * fitScale;\n const renderH = imgH * fitScale;\n ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);\n ctx.restore();\n }\n\n /**\n * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.\n */\n public async renderExport(\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: ExportOptions = {}\n ): Promise<HTMLCanvasElement> {\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const { crop, transform, annotations } = state;\n\n const region = crop\n ? clampCropToImage(crop, naturalWidth, naturalHeight)\n : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n\n const exportW = options.width || baseW;\n const exportH = options.height || baseH;\n\n const exportCanvas = this.createCanvas(exportW, exportH);\n const ctx = exportCanvas.getContext('2d');\n if (!ctx) return exportCanvas;\n\n let overlay: Annotation[] = annotations;\n let baseCanvas: HTMLCanvasElement;\n // A render of chosen layers (`layerIds`) always composites them; the legacy views describe the whole stack.\n if (!options.layerIds && this.chooseRoute(state, region, naturalWidth, naturalHeight) === 'legacy') {\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);\n } else {\n const layers = options.includeAnnotations === false ? removeAnnotationLayers(state.layers) : state.layers;\n const split = splitOverlayAnnotations(layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const rect = coveringRect(region);\n const comp = this.createCanvas(rect.width, rect.height);\n const cctx = comp.getContext('2d');\n if (cctx && typeof cctx.createImageData === 'function') {\n this.exportCompositor.compositeTiles(split.composite, rect, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {\n this.putTile(cctx, data, tx, ty, rect);\n });\n }\n baseCanvas = this.createCanvas(baseW, baseH);\n const bctx = baseCanvas.getContext('2d');\n if (bctx) {\n bctx.save();\n applyImageProjectionToContext(bctx, baseProjection(region, transform, baseW, baseH));\n bctx.beginPath();\n bctx.rect(region.x, region.y, region.width, region.height);\n bctx.clip();\n bctx.drawImage(comp, rect.x, rect.y);\n bctx.restore();\n this.applyPost(bctx, baseW, baseH, state);\n }\n }\n\n // Scale to target export dimensions\n ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);\n\n // Draw annotations (natural image space) through the same rotate/flip/crop projection\n if (options.includeAnnotations !== false && overlay.length > 0) {\n ctx.save();\n ctx.scale(exportW / baseW, exportH / baseH);\n applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n }\n ctx.restore();\n }\n\n return exportCanvas;\n }\n\n /**\n * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in\n * natural image pixel space of the current source image — the same space the crop box\n * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.\n */\n public async renderMask(\n state: ImageEditorState,\n options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }\n ): Promise<HTMLCanvasElement> {\n const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));\n const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));\n const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);\n const ctx = maskCanvas.getContext('2d');\n if (!ctx) return maskCanvas;\n\n // Background: unmasked (black)\n ctx.fillStyle = MASK_BLACK;\n ctx.fillRect(0, 0, naturalWidth, naturalHeight);\n\n const useCrop = options?.useCrop ?? true;\n const useAnnotations = options?.useAnnotations ?? true;\n\n // Crop box mask (if active)\n if (useCrop && state.crop) {\n ctx.fillStyle = MASK_WHITE;\n ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);\n }\n\n // Annotation masks (drawn in solid white, never in annotation colours)\n if (useAnnotations && state.annotations.length > 0) {\n for (const ann of state.annotations) {\n this.drawAnnotationMask(ctx, ann);\n }\n }\n\n // Anti-aliased edges produce grey pixels: threshold to pure black/white.\n const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);\n // The active selection (same natural space) is added to the mask.\n const selection =\n (options?.useSelection ?? true) &&\n state.selection?.bounds &&\n state.selection.width === naturalWidth &&\n state.selection.height === naturalHeight\n ? state.selection\n : null;\n if (imageData) {\n const d = imageData.data;\n for (let i = 0; i < d.length; i += 4) {\n const v = d[i] >= 128 || (selection !== null && selection.data[i >> 2] >= 128) ? 255 : 0;\n d[i] = v;\n d[i + 1] = v;\n d[i + 2] = v;\n d[i + 3] = 255;\n }\n ctx.putImageData(imageData, 0, 0);\n }\n\n return maskCanvas;\n }\n\n private drawAnnotationMask(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = MASK_WHITE;\n ctx.strokeStyle = MASK_WHITE;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n if (ann.points.length > 0) {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.points[0].x, ann.points[0].y);\n for (let i = 1; i < ann.points.length; i++) {\n ctx.lineTo(ann.points[i].x, ann.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n case 'circle': {\n ctx.beginPath();\n ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);\n ctx.fill();\n break;\n }\n case 'arrow':\n case 'line': {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.x, ann.y);\n ctx.lineTo(ann.endX, ann.endY);\n ctx.stroke();\n break;\n }\n case 'rect':\n case 'image':\n case 'text': {\n const b = getAnnotationBounds(ann);\n ctx.fillRect(b.x, b.y, b.width, b.height);\n break;\n }\n }\n ctx.restore();\n }\n}\n\nconst MASK_WHITE = '#ffffff';\nconst MASK_BLACK = '#000000';\n\nfunction getSourceSize(img: CanvasImageSource, fallbackW = 800, fallbackH = 600): { width: number; height: number } {\n const anyImg = img as any;\n return {\n width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,\n height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH,\n };\n}\n\nfunction getBaseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/** What the renderer asks its host (the controller) about layer pixels. */\nexport interface LayerSourcesHost {\n surfaceOf(layer: PixelLayer): PixelSurface;\n sourceIsOpaque(): boolean;\n version(): string;\n /** The document's global light for layer effects. */\n globalLight?(): GlobalLight | undefined;\n /** A document pattern for pattern effects (built-in patterns are found without it). */\n pattern?(id: string): EffectPattern | null;\n}\n\n/** The viewport's composite canvas and what each of its tiles holds. */\nclass CompositeCanvas {\n readonly tiles = new Map<TileKey, Uint8ClampedArray>();\n readonly histograms = new Map<TileKey, ImageHistogram>();\n version = 0;\n region: CropBox;\n constructor(readonly canvas: HTMLCanvasElement, readonly rect: CropBox) {\n this.region = rect;\n }\n\n /** Histogram of the composite over the region (per-tile parts are cached). */\n histogram(): ImageHistogram | null {\n if (!this.tiles.size) return null;\n const r = coveringRect(this.region);\n const parts: ImageHistogram[] = [];\n for (const [key, data] of this.tiles) {\n let h = this.histograms.get(key);\n if (!h) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const x0 = Math.max(r.x, tx * TILE);\n const y0 = Math.max(r.y, ty * TILE);\n const x1 = Math.min(r.x + r.width, tx * TILE + TILE);\n const y1 = Math.min(r.y + r.height, ty * TILE + TILE);\n h = emptyHistogram();\n if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });\n this.histograms.set(key, h);\n }\n parts.push(h);\n }\n return sumHistograms(parts);\n }\n}\n\nfunction removeAnnotationLayers(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: removeAnnotationLayers(l.children) } : l);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox, ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type SelectionMask } from './mask';\n\nexport interface ShapeSelectionOptions {\n /** Partial coverage along the edge (default true). */\n antiAlias?: boolean;\n}\n\ninterface Edge {\n top: number;\n bottom: number;\n ax: number;\n ay: number;\n bx: number;\n by: number;\n}\n\n/**\n * Even-odd polygon coverage over a `width × height` image: exact horizontal coverage on 4\n * sub-rows per pixel row (1 without anti-aliasing, where a pixel is in when its centre is).\n * Each sub-row only tests the edges that cross it (an active edge list over edges sorted by\n * their top), so the cost is about pixels + edges × rows they span.\n */\nexport function polygonCoverage(\n points: readonly ImagePoint[],\n width: number,\n height: number,\n antiAlias = true\n): Float32Array {\n const m = new Float32Array(width * height);\n if (points.length < 3 || width <= 0 || height <= 0) return m;\n const sub = antiAlias ? 4 : 1;\n const edges: Edge[] = [];\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;\n if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n }\n edges.sort((p, q) => p.top - q.top);\n let next = 0;\n let active: Edge[] = [];\n const xs: number[] = [];\n for (let py = 0; py < height; py++) {\n const row = py * width;\n for (let s = 0; s < sub; s++) {\n const y = py + (s + 0.5) / sub;\n while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);\n if (active.length) active = active.filter((e) => e.bottom > y);\n if (!active.length) continue;\n xs.length = 0;\n for (const e of active) {\n if (e.top <= y) xs.push(e.ax + ((y - e.ay) / (e.by - e.ay)) * (e.bx - e.ax));\n }\n xs.sort((a, b) => a - b);\n for (let k = 0; k + 1 < xs.length; k += 2) {\n const x0 = Math.max(0, xs[k]);\n const x1 = Math.min(width, xs[k + 1]);\n if (x1 <= x0) continue;\n const c0 = Math.floor(x0);\n const c1 = Math.min(width, Math.ceil(x1));\n for (let cx = c0; cx < c1; cx++) {\n let cov: number;\n if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);\n else {\n const c = cx + 0.5;\n cov = c >= x0 && c < x1 ? 1 : 0;\n }\n if (cov > 0) m[row + cx] += cov / sub;\n }\n }\n }\n }\n for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;\n return m;\n}\n\nfunction toMask(width: number, height: number, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */\nexport function polygonSelection(\n width: number,\n height: number,\n points: readonly ImagePoint[],\n options: ShapeSelectionOptions = {}\n): SelectionMask {\n return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));\n}\n\n/** Normalises a rectangle with a negative width or height. */\nexport function normalizeRect(rect: CropBox): CropBox {\n const x = rect.width < 0 ? rect.x + rect.width : rect.x;\n const y = rect.height < 0 ? rect.y + rect.height : rect.y;\n return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };\n}\n\n/** Rectangular marquee. Fractional edges get partial coverage when anti-aliased. */\nexport function rectSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n const r = normalizeRect(rect);\n const pts = [\n { x: r.x, y: r.y },\n { x: r.x + r.width, y: r.y },\n { x: r.x + r.width, y: r.y + r.height },\n { x: r.x, y: r.y + r.height },\n ];\n return polygonSelection(width, height, pts, options);\n}\n\n/** The ellipse inscribed in `rect` as a polygon fine enough that its chords stay within 0.05 px. */\nexport function ellipsePoints(rect: CropBox): ImagePoint[] {\n const r = normalizeRect(rect);\n const rx = r.width / 2;\n const ry = r.height / 2;\n const cx = r.x + rx;\n const cy = r.y + ry;\n const rmax = Math.max(rx, ry, 0.5);\n // Sagitta of a chord with angle step θ is r(1 − cos(θ/2)) ≈ rθ²/8 ≤ 0.05.\n const step = Math.sqrt(0.4 / rmax);\n const n = Math.min(4096, Math.max(16, Math.ceil((2 * Math.PI) / step)));\n const pts: ImagePoint[] = [];\n for (let i = 0; i < n; i++) {\n const t = (i / n) * 2 * Math.PI;\n pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });\n }\n return pts;\n}\n\n/** Elliptical marquee inscribed in `rect`. */\nexport function ellipseSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n return polygonSelection(width, height, ellipsePoints(rect), options);\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\n\n/** How the magic wand measures colour difference. */\nexport type ColorDistance = 'rgb' | 'perceptual';\n\nexport interface MagicWandOptions {\n /**\n * 0..255. `rgb`: the largest per-channel difference in levels (alpha included), as\n * Photoshop's wand. `perceptual`: CIE76 ΔE*ab, with 255 ≙ ΔE 100. Default 32.\n */\n tolerance?: number;\n /** Only the 4-connected region around the seed (default true); otherwise every similar pixel. */\n contiguous?: boolean;\n /** Soft one-pixel fringe on the edge (default true). */\n antiAlias?: boolean;\n /** Default `rgb`. */\n distance?: ColorDistance;\n}\n\nfunction checkPixels(p: PixelBuffer): number {\n const n = p.width * p.height;\n if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {\n throw new Error('Invalid pixel buffer size');\n }\n if (p.data.length < n * 4) throw new Error('Pixel data is shorter than width × height × 4');\n return n;\n}\n\nconst SRGB_TO_LINEAR = (() => {\n const t = new Float32Array(256);\n for (let i = 0; i < 256; i++) {\n const v = i / 255;\n t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n }\n return t;\n})();\n\nconst labF = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);\n\n/** CIE Lab (D65) of an 8-bit sRGB colour. */\nexport function srgb8ToLab(r: number, g: number, b: number): [number, number, number] {\n const rl = SRGB_TO_LINEAR[r & 255];\n const gl = SRGB_TO_LINEAR[g & 255];\n const bl = SRGB_TO_LINEAR[b & 255];\n const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;\n const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;\n const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;\n const fx = labF(x);\n const fy = labF(y);\n const fz = labF(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\n/** `similar(i)` for the wand: does pixel `i` match the seed colour within the tolerance? */\nfunction similarity(pixels: PixelBuffer, seed: number, tolerance: number, distance: ColorDistance): (i: number) => boolean {\n const d = pixels.data;\n const tr = d[seed * 4];\n const tg = d[seed * 4 + 1];\n const tb = d[seed * 4 + 2];\n const ta = d[seed * 4 + 3];\n const tol = Math.max(0, Math.floor(tolerance + 1e-3));\n if (distance === 'perceptual') {\n const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);\n const de = (Math.max(0, tolerance) * 100) / 255;\n const de2 = de * de + 1e-6;\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n // Copying a selection can leave RGB under zero alpha; hidden colours must not split a\n // transparent region.\n if (ta === 0 && a === 0) return true;\n if (Math.abs(a - ta) > tol) return false;\n const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);\n return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;\n };\n }\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n if (ta === 0 && a === 0) return true;\n return (\n Math.abs(d[p] - tr) <= tol &&\n Math.abs(d[p + 1] - tg) <= tol &&\n Math.abs(d[p + 2] - tb) <= tol &&\n Math.abs(a - ta) <= tol\n );\n };\n}\n\n/**\n * Softens hard edges: an edge pixel (one whose 3×3 neighbourhood differs from it) becomes\n * `½ + ½·avg` if selected, `½·avg` if not (neighbours outside the image count as the pixel).\n * Only the box `x0..x1 × y0..y1` (exclusive ends) is visited.\n */\nfunction antialiasMarks(marks: Uint8Array, w: number, h: number, x0: number, y0: number, x1: number, y1: number): Uint8Array {\n const out = marks.slice();\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = y * w + x;\n const v = marks[i];\n let sum = 0;\n let edge = false;\n for (let dy = -1; dy <= 1; dy++) {\n const ny = y + dy;\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx;\n const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];\n if (n !== v) edge = true;\n sum += n;\n }\n }\n if (edge) {\n const avg = sum / (9 * 255);\n out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);\n }\n }\n }\n return out;\n}\n\n/**\n * Magic wand: pixels similar to the seed pixel. Contiguous mode is a scanline span fill with an\n * explicit stack (fill a run, then scan the rows above and below it for new runs), so it is\n * linear in the pixels visited and never recurses. An empty selection when the seed is outside.\n */\nexport function magicWand(pixels: PixelBuffer, seed: ImagePoint, options: MagicWandOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h } = pixels;\n const sx = Math.floor(seed.x);\n const sy = Math.floor(seed.y);\n if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);\n const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? 'rgb');\n const marks = new Uint8Array(n);\n let bx0 = w;\n let by0 = h;\n let bx1 = -1;\n let by1 = -1;\n if (options.contiguous ?? true) {\n // Span fill. Each stack entry is a seed pixel index.\n const stack: number[] = [sy * w + sx];\n while (stack.length) {\n const idx = stack.pop()!;\n if (marks[idx]) continue;\n const y = (idx / w) | 0;\n const row = y * w;\n let l = idx - row;\n let r = l;\n while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;\n while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;\n marks.fill(255, row + l, row + r + 1);\n if (l < bx0) bx0 = l;\n if (r > bx1) bx1 = r;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n for (const ny of [y - 1, y + 1]) {\n if (ny < 0 || ny >= h) continue;\n const nrow = ny * w;\n let inRun = false;\n for (let nx = l; nx <= r; nx++) {\n const ok = !marks[nrow + nx] && similar(nrow + nx);\n if (ok && !inRun) stack.push(nrow + nx);\n inRun = ok;\n }\n }\n }\n } else {\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) {\n if (similar(row + x)) {\n marks[row + x] = 255;\n if (x < bx0) bx0 = x;\n if (x > bx1) bx1 = x;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n }\n }\n }\n }\n if (bx1 < 0) return createSelectionMask(w, h);\n if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);\n return createSelectionMask(\n w,\n h,\n antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))\n );\n}\n\n/** One Color Range sample: a colour (0..255 levels or `#rrggbb`) and, optionally, where it was picked. */\nexport interface ColorRangeSample {\n color: readonly [number, number, number] | string;\n at?: ImagePoint;\n}\n\nexport interface ColorRangeOptions {\n /** 0..200 levels (default 40): coverage falls off linearly to 0 at `fuzziness + 1` levels. */\n fuzziness?: number;\n /**\n * Localized Color Clusters: a sample with a position also fades with distance from it,\n * reaching 0 at this many pixels.\n */\n localized?: number;\n}\n\nfunction sampleColor(c: ColorRangeSample['color']): [number, number, number] {\n if (typeof c === 'string') {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) throw new Error(`Invalid colour \"${c}\"`);\n const v = parseInt(m[1], 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n }\n return [c[0], c[1], c[2]];\n}\n\n/**\n * Select › Color Range › Sampled Colors: each pixel takes its best match over the samples; a\n * match falls off linearly with the largest per-channel difference, `1 − d / (fuzziness + 1)`.\n * Transparent pixels are never selected.\n */\nexport function colorRange(pixels: PixelBuffer, samples: readonly ColorRangeSample[], options: ColorRangeOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h, data } = pixels;\n const f = Math.max(0, options.fuzziness ?? 40) + 1;\n const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));\n const radius = options.localized !== undefined ? Math.max(1, options.localized) : undefined;\n const out = new Uint8Array(n);\n if (!parsed.length) return createSelectionMask(w, h, out);\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (data[p + 3] <= 0) continue;\n const x = i % w;\n const y = (i - x) / w;\n let best = 0;\n for (const s of parsed) {\n const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));\n let k = Math.min(1, Math.max(0, 1 - d / f));\n if (k > 0 && radius !== undefined && s.at) {\n const dist = Math.hypot(x - s.at.x, y - s.at.y);\n k *= Math.min(1, Math.max(0, 1 - dist / radius));\n }\n if (k > best) best = k;\n }\n out[i] = coverageToByte(best);\n }\n return createSelectionMask(w, h, out);\n}\n\nexport type ToneRangePreset = 'shadows' | 'midtones' | 'highlights';\n\n/** Luma bounds (0..255, Rec. 601) of the tonal presets. */\nexport const TONE_RANGE_PRESETS: Record<ToneRangePreset, [number, number]> = {\n shadows: [0, 65],\n midtones: [105, 150],\n highlights: [190, 255],\n};\n\n/**\n * Select › Color Range › Shadows / Midtones / Highlights: pixels whose luma lies in `lo..hi` are\n * selected; outside, selection falls off linearly to 0 over `falloff` levels.\n */\nexport function toneRange(\n pixels: PixelBuffer,\n range: ToneRangePreset | readonly [number, number],\n falloff = 20\n): SelectionMask {\n const n = checkPixels(pixels);\n const [lo, hi] = typeof range === 'string' ? TONE_RANGE_PRESETS[range] : range;\n const ramp = Math.max(1e-3, falloff);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));\n const outside = l < lo ? lo - l : l > hi ? l - hi : 0;\n out[i] = coverageToByte(1 - outside / ramp);\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n\n/**\n * Select › Color Range › Reds … Magentas (`center` 0, 60, …, 300°): closeness of hue (full at\n * the centre, none 60° away) × chroma × (2 − max), so neutrals are never selected.\n */\nexport function hueRange(pixels: PixelBuffer, center: number): SelectionMask {\n const n = checkPixels(pixels);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const r = d[p] / 255;\n const g = d[p + 1] / 255;\n const b = d[p + 2] / 255;\n const mx = Math.max(r, g, b);\n const mn = Math.min(r, g, b);\n const chroma = mx - mn;\n if (chroma <= 0) continue;\n let hue: number;\n if (mx === r) hue = (((g - b) / chroma) % 6 + 6) % 6;\n else if (mx === g) hue = (b - r) / chroma + 2;\n else hue = (r - g) / chroma + 4;\n hue *= 60;\n let dh = (((hue - center) % 360) + 360) % 360;\n dh = Math.min(dh, 360 - dh);\n out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n","// Portions ported from photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\nimport { distanceTransform, gaussianBlur, greyMorphology, shiftCoverage } from './modify';\n\n/**\n * Refine Edge / Select and Mask parameters (Photoshop's units). Refinement is edge-aware: within\n * `radius` pixels of the mask's edge the coverage is re-estimated from the image with a colour\n * guided filter (He et al.), so a coarse or misaligned mask (e.g. an AI segmentation) follows\n * the real edges of the image.\n */\nexport interface RefineEdgeParams {\n /** Edge-detection band radius in pixels (0 = no edge detection). */\n radius?: number;\n /** Narrow the band where the image edge is sharp (hair stays soft, hard edges stay hard). */\n smartRadius?: boolean;\n /** 0..100: rounds jagged edges. */\n smooth?: number;\n /** Pixels of Gaussian softening. */\n feather?: number;\n /** 0..100: hardens soft edges. */\n contrast?: number;\n /** −100..100: moves the edge out (positive) or in; 100 % is `max(radius, 4)` pixels. */\n shiftEdge?: number;\n}\n\nexport const DEFAULT_REFINE_EDGE_PARAMS: Required<RefineEdgeParams> = {\n radius: 0,\n smartRadius: false,\n smooth: 0,\n feather: 0,\n contrast: 0,\n shiftEdge: 0,\n};\n\n/** Guided-filter regulariser (colour guide, samples in 0..1). */\nexport const GUIDED_FILTER_EPS = 1e-4;\n\nconst fin = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\n/** Clamped, complete parameters. */\nexport function normalizeRefineEdgeParams(p: RefineEdgeParams = {}): Required<RefineEdgeParams> {\n return {\n radius: Math.min(250, Math.max(0, fin(p.radius, 0))),\n smartRadius: p.smartRadius === true,\n smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),\n feather: Math.min(1000, Math.max(0, fin(p.feather, 0))),\n contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),\n shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0))),\n };\n}\n\nconst gfRadius = (p: Required<RefineEdgeParams>) => Math.min(64, Math.max(1, Math.ceil(p.radius)));\nconst smoothSigma = (p: Required<RefineEdgeParams>) => (p.smooth / 100) * 4;\nconst featherSigma = (p: Required<RefineEdgeParams>) => p.feather / 2;\nfunction shiftAmount(p: Required<RefineEdgeParams>): number {\n const px = (p.shiftEdge / 100) * Math.max(p.radius, 4);\n return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;\n}\n\n/** Pixels around the mask's content that refinement can read or write. */\nexport function refineEdgeHalo(params: RefineEdgeParams = {}): number {\n const p = normalizeRefineEdgeParams(params);\n const g = gfRadius(p);\n const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;\n return (\n edge +\n Math.ceil(3 * smoothSigma(p)) +\n Math.ceil(3 * featherSigma(p)) +\n Math.ceil(Math.abs(shiftAmount(p))) +\n 2\n );\n}\n\n/** Box mean of radius `r` (window clipped to the image, divided by its pixel count). O(1) per pixel. */\nexport function boxMean(src: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const tmp = new Float32Array(w * h);\n const pre = new Float64Array(Math.max(w, h) + 1);\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];\n for (let x = 0; x < w; x++) {\n const lo = Math.max(0, x - r);\n const hi = Math.min(w, x + r + 1);\n tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n const out = new Float32Array(w * h);\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];\n for (let y = 0; y < h; y++) {\n const lo = Math.max(0, y - r);\n const hi = Math.min(h, y + r + 1);\n out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n return out;\n}\n\n/**\n * Guided filter with a colour guide (He et al., Eq. 19–21: `a_k = (Σ_k + εU)⁻¹ cov_k(I, p)`):\n * filters `p` so that it is locally a linear function of the guide's colour, which makes the\n * output's edges follow the guide's edges. `guide` is three planes of 0..1 values.\n */\nexport function guidedFilterColor(\n guide: readonly [Float32Array, Float32Array, Float32Array],\n p: Float32Array,\n w: number,\n h: number,\n r: number,\n eps = GUIDED_FILTER_EPS\n): Float32Array {\n const n = w * h;\n const [c0, c1, c2] = guide;\n const prod = (a: ArrayLike<number>, b: ArrayLike<number>) => {\n const o = new Float32Array(n);\n for (let i = 0; i < n; i++) o[i] = a[i] * b[i];\n return o;\n };\n const m0 = boxMean(c0, w, h, r);\n const m1 = boxMean(c1, w, h, r);\n const m2 = boxMean(c2, w, h, r);\n const mp = boxMean(p, w, h, r);\n const mip0 = boxMean(prod(c0, p), w, h, r);\n const mip1 = boxMean(prod(c1, p), w, h, r);\n const mip2 = boxMean(prod(c2, p), w, h, r);\n const s00 = boxMean(prod(c0, c0), w, h, r);\n const s01 = boxMean(prod(c0, c1), w, h, r);\n const s02 = boxMean(prod(c0, c2), w, h, r);\n const s11 = boxMean(prod(c1, c1), w, h, r);\n const s12 = boxMean(prod(c1, c2), w, h, r);\n const s22 = boxMean(prod(c2, c2), w, h, r);\n const a0 = new Float32Array(n);\n const a1 = new Float32Array(n);\n const a2 = new Float32Array(n);\n const b = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];\n const rr = s00[i] - u0 * u0 + eps;\n const rg = s01[i] - u0 * u1;\n const rb = s02[i] - u0 * u2;\n const gg = s11[i] - u1 * u1 + eps;\n const gb = s12[i] - u1 * u2;\n const bb = s22[i] - u2 * u2 + eps;\n const v0 = mip0[i] - u0 * up;\n const v1 = mip1[i] - u1 * up;\n const v2 = mip2[i] - u2 * up;\n // Inverse of the symmetric 3×3 matrix.\n const i00 = gg * bb - gb * gb;\n const i01 = gb * rb - rg * bb;\n const i02 = rg * gb - gg * rb;\n const i11 = rr * bb - rb * rb;\n const i12 = rb * rg - rr * gb;\n const i22 = rr * gg - rg * rg;\n const det = rr * i00 + rg * i01 + rb * i02;\n let x0 = 0, x1 = 0, x2 = 0;\n if (Math.abs(det) >= 1e-30) {\n x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;\n x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;\n x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;\n }\n a0[i] = x0;\n a1[i] = x1;\n a2[i] = x2;\n b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;\n }\n const ma0 = boxMean(a0, w, h, r);\n const ma1 = boxMean(a1, w, h, r);\n const ma2 = boxMean(a2, w, h, r);\n const mb = boxMean(b, w, h, r);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];\n return out;\n}\n\n/**\n * Per-pixel transition width (pixels) of the guide's luminance: local range over local maximum\n * gradient in a window of radius `r` (a sharp step gives about 2, a ramp of width L about L).\n */\nexport function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array {\n const n = w * h;\n const y = new Float32Array(n);\n for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];\n const g = new Float32Array(n);\n for (let yy = 0; yy < h; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;\n const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;\n g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);\n }\n }\n const gm = greyMorphology(g, w, h, r, true);\n const hi = greyMorphology(y, w, h, r, true);\n const lo = greyMorphology(y, w, h, r, false);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);\n return out;\n}\n\n/**\n * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when\n * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,\n * contrast and shift edge, in that order.\n */\nexport function refineCoverage(\n guide: readonly [Float32Array, Float32Array, Float32Array] | null,\n m: Float32Array,\n w: number,\n h: number,\n params: RefineEdgeParams = {}\n): Float32Array {\n const p = normalizeRefineEdgeParams(params);\n let a: Float32Array = Float32Array.from(m);\n if (p.radius > 0 && guide) {\n const n = w * h;\n const boundary = new Uint8Array(n);\n let any = false;\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = y * w + x;\n const v = m[i] >= 0.5;\n const diff =\n (x > 0 && m[i - 1] >= 0.5 !== v) ||\n (x + 1 < w && m[i + 1] >= 0.5 !== v) ||\n (y > 0 && m[i - w] >= 0.5 !== v) ||\n (y + 1 < h && m[i + w] >= 0.5 !== v);\n if (diff) {\n boundary[i] = 1;\n any = true;\n }\n }\n }\n if (any) {\n const dist = distanceTransform(boundary, w, h);\n const r = gfRadius(p);\n const q = guidedFilterColor(guide, m, w, h, r);\n const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;\n for (let i = 0; i < n; i++) {\n const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;\n const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));\n if (wgt > 0) {\n const qi = Math.min(1, Math.max(0, q[i]));\n a[i] = m[i] + (qi - m[i]) * wgt;\n }\n }\n }\n }\n const ss = smoothSigma(p);\n if (ss >= 0.3) {\n a = gaussianBlur(a, w, h, ss, 'clamp');\n const gain = 1 + ss;\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n a = gaussianBlur(a, w, h, featherSigma(p), 'clamp');\n if (p.contrast > 0) {\n const gain = 1 / (1 - (0.99 * p.contrast) / 100);\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n const amount = shiftAmount(p);\n if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);\n return a;\n}\n\n/** Three 0..1 planes of a window of an RGBA buffer. */\nexport function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array] {\n const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };\n const n = box.width * box.height;\n const r = new Float32Array(n);\n const g = new Float32Array(n);\n const b = new Float32Array(n);\n const d = pixels.data;\n for (let y = 0; y < box.height; y++) {\n for (let x = 0; x < box.width; x++) {\n const i = y * box.width + x;\n const p = ((box.y + y) * pixels.width + box.x + x) * 4;\n r[i] = d[p] / 255;\n g[i] = d[p + 1] / 255;\n b[i] = d[p + 2] / 255;\n }\n }\n return [r, g, b];\n}\n\n/**\n * Refine Edge on a selection (or a coarse AI mask loaded as one) against `pixels` (the image,\n * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small\n * selection on a large image stays cheap.\n */\nexport function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params: RefineEdgeParams = {}): SelectionMask {\n const p = normalizeRefineEdgeParams(params);\n if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {\n throw new Error('Refine Edge needs pixels the size of the selection');\n }\n if (!mask.bounds) return mask;\n const halo = refineEdgeHalo(p);\n // The guided filter reads 2r beyond the band; the window must hold that too.\n const x0 = Math.max(0, mask.bounds.x - halo);\n const y0 = Math.max(0, mask.bounds.y - halo);\n const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);\n const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);\n const ww = x1 - x0;\n const wh = y1 - y0;\n const m = new Float32Array(ww * wh);\n for (let y = 0; y < wh; y++) {\n const src = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;\n }\n const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;\n const refined = refineCoverage(guide, m, ww, wh, p);\n const out = new Uint8Array(mask.width * mask.height);\n for (let y = 0; y < wh; y++) {\n const dst = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);\n }\n return createSelectionMask(mask.width, mask.height, out);\n}\n","// Portions ported from photocraft/crates/engine/src/transform_cmds.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Free Transform sessions: what a transform moves (a layer's pixels with its linked mask, a lone\n * unlinked mask, the selected pixels as a floating piece, a copy of them on a new layer, or just\n * the selection outline), a live preview while the geometry changes, and one undo step when it is\n * committed with full-quality resampling.\n */\n\nimport {\n TiledSurface,\n contentBounds,\n readRegion,\n surfaceFromPixels,\n writeRegion,\n type RasterRect,\n} from '../raster/surface';\nimport {\n compositeSurfaceOver,\n reduceSurface,\n transformSurface,\n TransformTooLargeError,\n warpSurface,\n isInterpolation,\n type Interpolation,\n type ResampleOptions,\n} from '../raster/transform';\nimport { multiplyHomographies, type Homography } from '../geom/homography';\nimport { findLayer, insertLayer, mapLayer, newLayerId, type ImageLayer, type LayerTarget, type PixelLayer, type RasterMask, type SmartObjectLayer } from './layers/model';\nimport { LayerOperationError, requireLayer, requireUnlocked } from './layers/ops';\nimport { contentsBox, isContentsDecoded, placementGeometry, renderSmartObject, setSmartPlacement } from './layers/smart';\nimport { createSelectionMask, type SelectionMask } from './selection/mask';\nimport {\n geometryHomography,\n hasWarp,\n identityTransform,\n isIdentityTransform,\n transformMap,\n type FreeTransformGeometry,\n} from './transform';\nimport type { CropBox } from './types';\n\n/**\n * What a Free Transform moves:\n * - `layer`: the layer's pixels with its linked mask (or, with `channel: 'mask'` on an unlinked\n * mask, the mask alone);\n * - `selection`: the selected pixels (and the same part of a linked mask), lifted and committed\n * back into the layer; the selection outline moves with them;\n * - `selection-copy`: Layer via Copy, then the copy is transformed on a new layer above;\n * - `selection-outline`: Transform Selection, only the outline;\n * - `auto` (default): `selection` when something is selected, else `layer`.\n */\nexport type FreeTransformContent = 'auto' | 'layer' | 'selection' | 'selection-copy' | 'selection-outline';\n\nexport const FREE_TRANSFORM_CONTENTS: readonly FreeTransformContent[] = ['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'];\n\nexport interface FreeTransformInit {\n /** The layer (default the active one); not needed for `selection-outline`. */\n layerId?: string;\n content?: FreeTransformContent;\n /**\n * `pixels` (default) moves the pixels with a linked mask; `mask` moves an unlinked mask alone (a\n * linked one moves with its pixels). Defaults to the active target on the active layer.\n */\n channel?: LayerTarget;\n /** Resampling of the committed result (default bicubic). */\n interpolation?: Interpolation;\n}\n\n/** How a live preview is drawn (the UI knows these, the controller doesn't). */\nexport interface FreeTransformPreview {\n /** Screen pixels per document pixel: below 1 the preview samples a reduced copy in blocks. */\n scale?: number;\n /** Only this part of the document (the visible area) is resampled. */\n clip?: CropBox;\n}\n\n/** What a committed transform changed. */\nexport interface FreeTransformResult {\n /** The layer that holds the result (the new layer for `selection-copy`; `null` for `selection-outline`). */\n layerId: string | null;\n content: Exclude<FreeTransformContent, 'auto'>;\n /** What moved. */\n moved: Array<'pixels' | 'mask' | 'selection'>;\n /** The frame the transform started from (document pixels). */\n box: CropBox;\n /** Where the frame's corners went. */\n corners: FreeTransformGeometry['corners'];\n /** Document bounds of the moved content after the transform (pixels, or the mask or the outline). */\n bounds: CropBox | null;\n /** False when the geometry was the identity and nothing was recorded. */\n changed: boolean;\n}\n\n/** @internal What the session needs from the controller. */\nexport interface FreeTransformHost {\n layers(): ImageLayer[];\n activeLayerId(): string | null;\n activeTarget(): LayerTarget;\n selection(): SelectionMask | null;\n canvas(): { width: number; height: number };\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n pixelsOf(layer: PixelLayer | SmartObjectLayer): TiledSurface;\n /** Opaque pre-transform snapshot. */\n capture(): unknown;\n /** Shows a live state (no history). */\n show(layers: ImageLayer[], selection: SelectionMask | null, activeLayerId: string | null): void;\n /** Records `before` as the undo step of the state now shown. */\n record(before: unknown): void;\n /** Puts `before` back (no history). */\n restore(before: unknown): void;\n}\n\n/** A surface that moves: in layer space (`offset` to document), composited back or replacing. */\ninterface Part {\n what: 'pixels' | 'mask' | 'selection';\n offset: { x: number; y: number };\n /** RGBA (pixels, or a mask part as grey + alpha) or one channel (a whole mask, the outline). */\n moving: TiledSurface;\n /** What stays: composited under the moved part (`null`: the moved part replaces the surface). */\n base: TiledSurface | null;\n /** Reduced copies for previews, by factor. */\n reduced: Map<number, TiledSurface>;\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\nconst T = (x: number, y: number): Homography => [1, 0, x, 0, 1, y, 0, 0, 1];\n\n/** Preview block size for a screen scale: 1 at 100 % and above, then powers of two up to 8. */\nexport function previewStep(scale: number | undefined): number {\n if (!scale || !(scale > 0) || scale >= 1) return 1;\n return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));\n}\n\nfunction toDoc(r: RasterRect | null, o: { x: number; y: number }): CropBox | null {\n return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;\n}\n\nfunction selectionSurface(sel: SelectionMask): TiledSurface {\n return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);\n}\n\n/**\n * Lifts the selected part of an RGBA surface (layer space): the lifted pixels (alpha × coverage)\n * and the rest (alpha × (1 − coverage); fully lifted pixels become transparent black).\n */\nfunction liftPixels(surface: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, area);\n const kept = new Uint8ClampedArray(px);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const i = (y * area.width + x) * 4;\n if (k <= 0) {\n px[i] = px[i + 1] = px[i + 2] = px[i + 3] = 0;\n continue;\n }\n px[i + 3] = px[i + 3] * k;\n if (!px[i + 3]) px[i] = px[i + 1] = px[i + 2] = 0;\n kept[i + 3] = kept[i + 3] * (1 - k);\n if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };\n}\n\n/**\n * Lifts the selected part of a mask (layer space): the values as grey + alpha (alpha = coverage)\n * and the rest (the selected values replaced by the default). Everything selected moves,\n * default-valued parts included, so the mask stays aligned with the moved pixels.\n */\nfunction liftMask(mask: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const v = readRegion(mask, area);\n const def = mask.defaultValue;\n const lifted = new Uint8ClampedArray(area.width * area.height * 4);\n const rest = new Uint8ClampedArray(v.length);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const i = y * area.width + x;\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const g = v[i];\n if (k > 0) {\n lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;\n lifted[i * 4 + 3] = k * 255;\n }\n rest[i] = g * (1 - k) + def * k;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };\n}\n\n/**\n * A Free Transform in progress (see `ImageEditorController.beginFreeTransform`). Change the\n * geometry with `update` (each change shows a live preview without history), then `commit()`\n * (one undo step, resampled at full quality) or `cancel()` (everything back as it was).\n */\nexport class FreeTransformSession {\n readonly content: Exclude<FreeTransformContent, 'auto'>;\n /** The layer being transformed (`null` for `selection-outline`). */\n readonly layerId: string | null;\n /** The new layer of a `selection-copy`. */\n readonly copyId: string | null = null;\n /** The frame (document pixels). */\n readonly box: CropBox;\n /** What moves. */\n readonly moves: ReadonlyArray<'pixels' | 'mask' | 'selection'>;\n /**\n * The geometry the transform starts from: the identity over `box`, or for a smart object its\n * placement (`box` is then the contents' rectangle in contents pixels, and the transform edits\n * the placement without resampling anything twice).\n */\n readonly startGeometry: FreeTransformGeometry;\n /** True when the layer is a smart object (the transform sets its placement). */\n readonly smartObject: boolean = false;\n private geometryValue: FreeTransformGeometry;\n private readonly smart: SmartObjectLayer | null = null;\n private readonly smartReduced = new Map<number, TiledSurface>();\n private readonly startInterp: Interpolation;\n private smartMasksMoved = false;\n private interp: Interpolation;\n private readonly parts: Part[] = [];\n private readonly before: unknown;\n private readonly startLayers: ImageLayer[];\n private readonly startSelection: SelectionMask | null;\n private readonly startActive: string | null;\n /** The stack this session last showed: anything else means the document changed under it. */\n private shown: ImageLayer[];\n private closed = false;\n\n constructor(\n private readonly host: FreeTransformHost,\n init: FreeTransformInit = {}\n ) {\n if (init.interpolation !== undefined && !isInterpolation(init.interpolation)) fail(`Unknown interpolation \"${String(init.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = init.interpolation ?? 'bicubic';\n const sel = host.selection();\n const selected = Boolean(sel?.bounds);\n let content = init.content ?? 'auto';\n if (!(['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'] as const).includes(content)) {\n fail(`Unknown content \"${String(content)}\"; use auto, layer, selection, selection-copy or selection-outline.`);\n }\n const targetId = init.layerId ?? host.activeLayerId();\n const target = targetId && content !== 'selection-outline' ? findLayer(host.layers(), targetId)?.layer : undefined;\n const smartTarget = target?.type === 'smart' && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : 'pixels')) !== 'mask';\n // A smart object transforms as a whole, selection or not (as in Photoshop).\n if (content === 'auto') content = selected && !smartTarget ? 'selection' : 'layer';\n this.content = content;\n if (content !== 'layer' && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);\n const layers = host.layers();\n this.startLayers = layers;\n this.startSelection = sel;\n this.startActive = host.activeLayerId();\n this.before = host.capture();\n const moves: Array<'pixels' | 'mask' | 'selection'> = [];\n let box: CropBox | null = null;\n\n if (content === 'selection-outline') {\n this.layerId = null;\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n box = { ...sel!.bounds! };\n } else {\n const id = init.layerId ?? host.activeLayerId();\n if (!id) fail('No layer given and none is active; pass layerId (see list_layers).');\n const layer = requireLayer(layers, id!).layer;\n this.layerId = layer.id;\n const mask = layer.mask && !layer.mask.source ? layer.mask : undefined;\n const channel: LayerTarget = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : 'pixels');\n if (channel !== 'pixels' && channel !== 'mask') fail(`Unknown channel \"${String(channel)}\"; use pixels or mask.`);\n const linked = mask?.linked !== false;\n const maskAlone = channel === 'mask' && (!linked || layer.type !== 'pixel');\n if (channel === 'mask' && !mask) fail(layer.mask?.source ? `Layer \"${layer.name || id}\" has a depth mask, which is generated, not transformed.` : `Layer \"${layer.name || id}\" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);\n if (!maskAlone && layer.type !== 'pixel' && layer.type !== 'smart') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);\n if (!layer.visible && content !== 'selection-copy') fail(`Layer \"${layer.name || id}\" is hidden; show it before you transform it.`);\n if (content !== 'selection-copy') {\n requireUnlocked(layer, 'position', 'transform it');\n if (!maskAlone) requireUnlocked(layer, 'pixels', 'transform it');\n }\n const o = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const pixels = layer.type === 'pixel' ? host.pixelsOf(layer) : null;\n if (layer.type === 'smart' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n if (!isContentsDecoded(layer.contents)) fail(`The contents of \"${layer.name || id}\" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);\n this.smart = layer;\n this.smartObject = true;\n this.startGeometry = placementGeometry(layer);\n if (init.interpolation === undefined && layer.placement.interpolation) this.interp = layer.placement.interpolation;\n this.startInterp = this.interp;\n this.box = contentsBox(layer.contents);\n this.moves = mask && linked ? ['pixels', 'mask'] : ['pixels'];\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n return;\n }\n if (maskAlone) {\n if (content === 'selection-copy') fail('Layer via Copy copies pixels; transform a mask with content layer or selection.');\n if (content === 'selection') {\n const { lifted, rest } = liftMask(mask!.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: lifted, base: rest, reduced: new Map() });\n box = toDoc(contentBounds(lifted), o);\n } else {\n this.parts.push({ what: 'mask', offset: o, moving: mask!.surface, base: null, reduced: new Map() });\n box = toDoc(contentBounds(mask!.surface), o);\n }\n moves.push('mask');\n } else if (content === 'layer') {\n this.parts.push({ what: 'pixels', offset: o, moving: pixels!, base: null, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(pixels!), o);\n if (mask && linked) {\n this.parts.push({ what: 'mask', offset: o, moving: mask.surface, base: null, reduced: new Map() });\n moves.push('mask');\n if (!box) box = toDoc(contentBounds(mask.surface), o);\n }\n } else if (content === 'selection') {\n const { lifted, rest } = liftPixels(pixels!, sel!, o);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: rest, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(lifted), o);\n if (mask && linked) {\n const m = liftMask(mask.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: m.lifted, base: m.rest, reduced: new Map() });\n moves.push('mask');\n }\n } else {\n // selection-copy: the copy goes on a new layer above, with the source layer's offset.\n const { lifted } = liftPixels(pixels!, sel!, o);\n box = toDoc(contentBounds(lifted), o);\n if (box) {\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: `${layer.name || 'Layer'} copy`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: lifted,\n offset: { ...o },\n };\n (this as { copyId: string | null }).copyId = copy.id;\n const loc = findLayer(layers, layer.id)!;\n this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: null, reduced: new Map() });\n moves.push('pixels');\n }\n }\n if (content !== 'layer') {\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n }\n }\n if (!box && moves[0] === 'mask') fail('The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).');\n if (!box) fail(content === 'layer' ? 'The layer has no pixels to transform.' : 'The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).');\n this.box = box!;\n this.moves = moves;\n this.startGeometry = identityTransform(box!);\n this.startInterp = this.interp;\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n if (this.copyId) host.show(this.startLayers, sel, this.copyId);\n this.shown = host.layers();\n }\n\n /** The current geometry. */\n get geometry(): FreeTransformGeometry {\n return this.geometryValue;\n }\n\n get interpolation(): Interpolation {\n return this.interp;\n }\n\n get isOpen(): boolean {\n return !this.closed;\n }\n\n private check(): void {\n if (this.closed) fail('This transform has already been committed or cancelled; begin a new one.');\n if (this.host.layers() !== this.shown) {\n this.closed = true;\n fail('The document changed while the transform was open, so it was abandoned; begin a new one.');\n }\n }\n\n /**\n * Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,\n * shows the result live (no history). Returns the geometry.\n */\n update(changes: { corners?: FreeTransformGeometry['corners']; warp?: FreeTransformGeometry['warp']; interpolation?: Interpolation }, preview: FreeTransformPreview | false = {}): FreeTransformGeometry {\n this.check();\n if (changes.interpolation !== undefined) {\n if (!isInterpolation(changes.interpolation)) fail(`Unknown interpolation \"${String(changes.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = changes.interpolation;\n }\n const next: FreeTransformGeometry = {\n box: this.box,\n corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })) as unknown as FreeTransformGeometry['corners'],\n warp: changes.warp === undefined ? this.geometryValue.warp ?? null : changes.warp,\n };\n if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail('corners must be four finite points: top-left, top-right, bottom-right, bottom-left.');\n if (!transformMap(next)) fail('These corners fold the frame over itself or through the horizon; keep the quad convex.');\n this.geometryValue = next;\n if (preview !== false) this.render(preview);\n return next;\n }\n\n /** Resamples every part for the current geometry and shows it (or returns it, for commit). */\n private resample(final: boolean, preview: FreeTransformPreview = {}): { layers: ImageLayer[]; selection: SelectionMask | null } {\n if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };\n const g = this.geometryValue;\n const identity = isIdentityTransform(g);\n const warp = hasWarp(g);\n const h = warp ? null : geometryHomography(g);\n const f = warp ? transformMap(g) : null;\n const step = final ? 1 : previewStep(preview.scale);\n const interpolation: Interpolation = final ? this.interp : this.interp === 'nearest' ? 'nearest' : 'bilinear';\n let layers = this.startLayers;\n let selection = this.startSelection;\n const targetId = this.copyId ?? this.layerId;\n for (const part of this.parts) {\n const o = part.offset;\n let moved: TiledSurface;\n if (identity) moved = part.moving;\n else {\n let src = part.moving;\n let k = 1;\n if (step > 1) {\n k = step;\n let r = part.reduced.get(k);\n if (!r) {\n r = reduceSurface(part.moving, k);\n part.reduced.set(k, r);\n }\n src = r;\n }\n const opts: ResampleOptions = { interpolation, step: step > 1 ? step : undefined };\n if (preview.clip && !final) opts.clip = { x: Math.floor(preview.clip.x - o.x), y: Math.floor(preview.clip.y - o.y), width: Math.ceil(preview.clip.width) + 1, height: Math.ceil(preview.clip.height) + 1 };\n try {\n if (f) {\n moved = warpSurface(src, (p) => {\n const q = f({ x: p.x * k + o.x, y: p.y * k + o.y });\n return q && { x: q.x - o.x, y: q.y - o.y };\n }, opts);\n } else {\n // Layer space → document → transform → layer space, from the reduced copy's pixels.\n const hl = multiplyHomographies(multiplyHomographies(multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], T(o.x, o.y)), h!), T(-o.x, -o.y));\n moved = transformSurface(src, hl, opts);\n }\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;\n if (part.what === 'selection') {\n const { width, height } = this.host.canvas();\n const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);\n const next = createSelectionMask(width, height, data);\n selection = next.bounds ? next : null;\n } else if (part.what === 'pixels') {\n layers = mapLayer(layers, targetId!, (l) => {\n const next = { ...(l as PixelLayer), surface: result };\n delete next.source;\n return next;\n });\n } else {\n layers = mapLayer(layers, targetId!, (l) => ({ ...l, mask: { ...(l.mask as RasterMask), surface: result } }) as ImageLayer);\n }\n }\n return { layers, selection };\n }\n\n /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */\n private resampleSmart(final: boolean, preview: FreeTransformPreview): ImageLayer[] {\n const layer = this.smart!;\n const g = this.geometryValue;\n const placement = { corners: g.corners, warp: g.warp ?? null, interpolation: this.interp };\n const canvas = this.host.canvas();\n try {\n if (final) {\n const res = setSmartPlacement(this.startLayers, layer.id, placement, canvas);\n this.smartMasksMoved = res.masksMoved;\n return res.layers;\n }\n const step = previewStep(preview.scale);\n const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });\n return mapLayer(this.startLayers, layer.id, (l) => ({ ...(l as SmartObjectLayer), placement, surface }));\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n\n /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */\n private isChanged(): boolean {\n const g = this.geometryValue;\n if (!this.smart) return !isIdentityTransform(g);\n const a = this.startGeometry;\n const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);\n const warp = (w: FreeTransformGeometry['warp'], box: CropBox) => (w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : '');\n return !same || warp(g.warp, g.box) !== warp(a.warp, a.box) || this.interp !== this.startInterp;\n }\n\n private render(preview: FreeTransformPreview): void {\n const { layers, selection } = this.resample(false, preview);\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.shown = this.host.layers();\n }\n\n /** Finishes the transform with full-quality resampling, as one undo step. */\n commit(): FreeTransformResult {\n this.check();\n const changed = this.isChanged() || Boolean(this.copyId);\n if (!changed) {\n this.closed = true;\n this.host.restore(this.before);\n return this.result(false, this.startLayers, this.startSelection);\n }\n const { layers, selection } = this.resample(true);\n this.closed = true;\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.host.record(this.before);\n return this.result(true, layers, selection);\n }\n\n /** Abandons the transform and puts everything back (no history). */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.host.restore(this.before);\n }\n\n private result(changed: boolean, layers: ImageLayer[], selection: SelectionMask | null): FreeTransformResult {\n let bounds: CropBox | null = null;\n const id = this.copyId ?? this.layerId;\n const layer = id ? findLayer(layers, id)?.layer : undefined;\n if (this.moves.includes('pixels') && layer?.type === 'pixel') bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);\n else if (this.moves.includes('pixels') && layer?.type === 'smart') bounds = contentBounds(layer.surface);\n else if (this.moves.includes('mask') && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 });\n else if (this.moves.includes('selection')) bounds = selection?.bounds ? { ...selection.bounds } : null;\n return {\n layerId: this.copyId ?? this.layerId,\n content: this.content,\n moved: this.smart ? this.moves.filter((m) => m !== 'mask' || this.smartMasksMoved) : [...this.moves],\n box: { ...this.box },\n corners: this.geometryValue.corners,\n bounds,\n changed,\n };\n }\n}\n","import {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentParamsMap,\n Annotation,\n CreateAnnotationPayload,\n AspectRatio,\n CropBox,\n DepthMaskOptions,\n ExportOptions,\n ImageAdjustments,\n ImageDimensions,\n ImageEditorFit,\n ImageEditorOptions,\n ImageEditorState,\n ImageEditorTool,\n ImageTransform,\n ImagePoint,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { ImageEditorRenderer, RenderOptions } from './renderer';\nimport { clampCropToImage, isQuarterTurnSwapped } from './geometry';\nimport { LATEST_PROCESS_VERSION, LEGACY_PROCESS_VERSION, normalizeProcessVersion } from './color';\nimport { createAdjustmentLayer, normalizeAdjustmentLayer, normalizeAdjustmentLayers } from './adjustment-layers';\nimport type { BlendMode } from './blend';\nimport type { ImageHistogram } from './histogram';\nimport { applyAdjustments } from './filters';\nimport {\n combineSelections,\n createSelectionMask,\n deserializeSelection,\n fullSelection,\n invertSelection as invertSelectionMask,\n resizeSelection,\n selectionFromPixels,\n serializeSelection,\n type PixelBuffer,\n type SelectionChannel,\n type SelectionMask,\n type SelectionMode,\n} from './selection/mask';\nimport { ellipseSelection, polygonSelection, rectSelection, type ShapeSelectionOptions } from './selection/shapes';\nimport { colorRange, magicWand, type ColorRangeOptions, type ColorRangeSample, type MagicWandOptions } from './selection/wand';\nimport { modifySelection as modifySelectionMask, type SelectionModifyOp } from './selection/modify';\nimport { refineSelection as refineSelectionMask, type RefineEdgeParams } from './selection/refine';\nimport type { LayerMaskProps } from './selection/layer-mask';\nimport type { AdjustmentLayerMask } from './types';\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES, DEFAULT_TRANSFORM_VALUES } from './layers/defaults';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type GroupLayer,\n type LayerBlendMode,\n type ImageLayer,\n type LayerFilter,\n type LayerTarget,\n type PixelLayer,\n type RasterMask,\n type SmartContents,\n type SmartFilter,\n type SmartObjectLayer,\n type SmartPlacement,\n} from './layers/model';\nimport {\n addSmartFilter as addSmartFilterOp,\n contentsSurface,\n convertToSmartObject as convertToSmartObjectOp,\n copySmartObject as copySmartObjectOp,\n documentContents,\n instancesOf,\n isContentsDecoded,\n moveSmartFilter as moveSmartFilterOp,\n normalizeSmartFilter,\n offsetSmartObject,\n placementGeometry,\n rasterContents,\n rasterizeSmartObject as rasterizeSmartObjectOp,\n removeSmartFilter as removeSmartFilterOp,\n replaceContents as replaceContentsOp,\n requireSmartObject,\n setSmartFilterMask as setSmartFilterMaskOp,\n setSmartFiltersEnabled as setSmartFiltersEnabledOp,\n setSmartPlacement,\n updateSmartFilter as updateSmartFilterOp,\n withFilterMaskSurface,\n} from './layers/smart';\nimport {\n BACKGROUND_LAYER_ID,\n BACKGROUND_LAYER_NAME,\n BLUR_FILTER_ID,\n DEVELOP_FILTER_ID,\n adjustmentNodeFromLegacy,\n annotationLayerFor,\n annotationLayerName,\n depthRasterMask,\n developTarget,\n isDefaultDepthMask,\n layersFromLegacy,\n legacyMaskOf,\n legacyView,\n rasterMaskFromLegacy,\n type LayerStack,\n} from './layers/migrate';\nimport {\n LAYERED_SCHEMA_VERSION,\n deserializeLayers,\n deserializePatterns,\n serializePatterns,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayeredImageEditorState,\n} from './layers/serialize';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport {\n SurfaceDraft,\n TILE,\n collectTiles,\n contentBounds,\n emptySurface,\n packRgba,\n readPixel,\n readRegion,\n solidSurface,\n surfaceFromPixels,\n tilesInRect,\n translateSurface,\n writeRegion,\n type PixelSurface,\n type Tile,\n type TiledSurface,\n type TileKey,\n TiledSurface as TiledSurfaceClass,\n finishTile,\n tileKey,\n} from '../raster/surface';\nimport {\n LayerOperationError,\n addLayerEffect as addLayerEffectOp,\n addLayerFilter as addLayerFilterOp,\n layerEffectsOf,\n removeLayerEffect as removeLayerEffectOp,\n setLayerEffects as setLayerEffectsOp,\n setLayerEffectsEnabled as setLayerEffectsEnabledOp,\n updateLayerEffect as updateLayerEffectOp,\n duplicateLayer as duplicateLayerOp,\n groupLayers as groupLayersOp,\n moveLayer as moveLayerOp,\n normalizeFilter,\n offsetLayer as offsetLayerOp,\n parseBlend,\n placeLayer,\n removeLayer as removeLayerOp,\n removeLayerFilter as removeLayerFilterOp,\n requireLayer,\n requireUnlocked,\n setLayerMask as setLayerMaskOp,\n setLayerProperties as setLayerPropertiesOp,\n ungroupLayer as ungroupLayerOp,\n unionRect,\n updateLayerFilter as updateLayerFilterOp,\n type LayerPlacement,\n type LayerProperties,\n} from './layers/ops';\nimport { coveringRect } from './layers/render';\nimport {\n CoverageMap,\n DEFAULT_BRUSH,\n DEFAULT_MIXER,\n EMPTY_MIXER_LOAD,\n MixerStroke,\n StrokeCoverage,\n applyStrokeToTile,\n cleanMixer,\n loadMixer,\n normalizeBrush,\n normalizeMixer,\n type BrushSettings,\n type MixerLoad,\n type MixerSettings,\n type MixerTarget,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, encodePng, isPng, parseDataUrl } from '../raster/png';\nimport type { Interpolation } from '../raster/transform';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd } from './psd/export';\nimport { FreeTransformSession, type FreeTransformHost, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n anchorPoint,\n applyParams,\n cornersFromMatrix,\n describeTransform,\n flipCorners,\n identityTransform,\n matrixFromParams,\n transformedBox,\n isValidWarp,\n presetWarp,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpPreset,\n} from './transform';\nimport { WARP_STYLES } from '../geom/warp';\nimport { applyMatrix, multiplyMatrices, translateMatrix } from '../geom/affine';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { DEFAULT_GLOBAL_LIGHT, effectsMargin, type GlobalLight, type LayerEffect, type LayerEffectKind, type LayerEffects } from '../raster/effects/model';\n\n/** What `getSmartObject` reports. */\nexport interface SmartObjectInfo {\n layerId: string;\n contents: {\n /** Shared by linked instances. */\n id: string;\n name: string;\n kind: 'raster' | 'document';\n /** Pixel size of the contents. */\n width: number;\n height: number;\n /** False for a file that could not be decoded here (the layer shows its stored pixels). */\n decoded: boolean;\n /** The type of the file the contents came from (`psb`, `png`, `jpg`, …), or `null`. */\n fileType: string | null;\n /** The external file of linked contents, or `null` for embedded ones. */\n linkedPath: string | null;\n /** Every smart object placed from these contents (this one included), bottom first. */\n instances: string[];\n /** Layers in a nested document (0 for raster contents). */\n layerCount: number;\n };\n placement: {\n /** Where the contents' corners land: top-left, top-right, bottom-right, bottom-left (document pixels). */\n corners: Quad;\n /** The warp mesh in contents pixels, or `null`. */\n warp: WarpMesh | null;\n interpolation: Interpolation;\n /** False for distort, perspective and warp (then only corners and bounds describe it). */\n affine: boolean;\n /** Percent of the contents' own size. */\n scaleX: number;\n scaleY: number;\n rotate: number;\n skewX: number;\n /** Where the contents' centre is (document pixels). */\n center: { x: number; y: number };\n /** Bounds of the placed outline. */\n bounds: CropBox | null;\n };\n smartFilters: {\n enabled: boolean;\n /** Bottom (applied first) to top. Photoshop filters we keep but do not draw have kind `psd`. */\n filters: Array<Record<string, unknown>>;\n mask: { enabled: boolean; linked: boolean; density: number; feather: number; bounds: CropBox | null; defaultValue: number } | null;\n };\n /** Document bounds of the rendered pixels. */\n bounds: CropBox | null;\n}\n\n/** The document set aside while smart object contents are open. */\ninterface SavedDocument {\n state: ImageEditorState;\n undo: HistoryEntry[];\n redo: HistoryEntry[];\n initial: HistoryEntry;\n element: CanvasImageSource | null;\n samplePixels: PixelBuffer | null;\n samplePixelsOverride: boolean;\n psdResolution: number | undefined;\n}\n\n/** A nested document's layers as `exportPsd` takes them (filters baked, smart objects and annotations as their pixels). */\nfunction prepareNestedForPsd(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n let next: ImageLayer = l;\n if (l.type === 'group') next = { ...l, children: prepareNestedForPsd(l.children) };\n else if (l.type === 'smart') next = rasterizeSmartObjectOp([l], l.id).layer;\n else if (l.type === 'pixel' && l.filters?.length) {\n const p: PixelLayer = { ...l };\n if (l.filters.some(isActiveFilter)) {\n const b = l.surface.bounds;\n if (b) {\n const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };\n const plain: PixelLayer = { ...l, visible: true, opacity: 1, blend: 'normal' };\n delete plain.mask;\n delete plain.clipped;\n delete plain.effects;\n delete plain.fillOpacity;\n const data = new LayerCompositor().composite([plain], r, { width: r.x + r.width, height: r.y + r.height });\n p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);\n }\n }\n delete p.filters;\n next = p;\n }\n if (next.mask?.source) {\n next = { ...next } as ImageLayer;\n delete next.mask;\n }\n out.push(next);\n }\n return out;\n}\n\n/** Latest serialized schema version (`3`: layers). Documents with the old shape still write 1 or 2. */\nexport const IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;\n\n/** Changes to an adjustment layer; `params` is merged into the current parameters. */\nexport interface AdjustmentLayerUpdate<K extends AdjustmentKind = AdjustmentKind> {\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n name?: string;\n /** A layer mask, or `null` to remove it. */\n mask?: AdjustmentLayerMask | null;\n}\n\n/** Options shared by the selection commands. */\nexport interface SelectionCommandOptions {\n /** How the result combines with the current selection (default `replace`). */\n mode?: SelectionMode;\n /** Feather the new shape by this many pixels before combining (default 0). */\n feather?: number;\n}\n\n/** Where an AI result goes (see `applyInpaintedImage`, `applyBackgroundRemovedImage`). */\nexport interface AIResultOptions {\n /** `new-layer`: a new layer (inpainting) or a mask (background removal); `replace`: the Background's pixels. Default: by `isMultiLayer`. */\n target?: 'new-layer' | 'replace';\n}\n\n/** Where the selection tools read colours from. */\nexport interface SampleOptions {\n /**\n * `composite` (default): the image, as before (with several layers: the composite of the pixel\n * layers, or with `sampleMerged` of every layer); `active-layer`: only the active pixel layer.\n */\n sample?: 'composite' | 'active-layer';\n /** Sample the adjusted image (base adjustments and layers) instead of the source pixels. */\n sampleMerged?: boolean;\n /** Pixels to use instead of reading the source image (natural size, RGBA). */\n pixels?: PixelBuffer;\n}\n\n/** A new adjustment layer: its kind plus optional initial values and position. */\nexport interface AdjustmentLayerInit<K extends AdjustmentKind = AdjustmentKind> extends AdjustmentLayerUpdate<K> {\n kind: K;\n id?: string;\n /** Put the new layer directly above this layer (in its group). */\n aboveId?: string;\n /** Put the new layer at the top of this group. */\n parentId?: string;\n}\n\n/** A new pixel layer: empty, a solid colour over the canvas, or given pixels, plus its properties and position. */\nexport interface PixelLayerInit extends LayerPlacement {\n id?: string;\n name?: string;\n /** Fill the canvas with this colour (`#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` 0..255). */\n color?: string | number[];\n /** RGBA pixels placed at `x`, `y` (document pixels). */\n pixels?: PixelBuffer;\n x?: number;\n y?: number;\n visible?: boolean;\n /** 0..1 (default 1). */\n opacity?: number;\n fillOpacity?: number;\n blend?: BlendMode;\n}\n\n/** The paint tools' options (view state). */\nexport interface PaintOptions {\n brush: BrushSettings;\n /** `#rrggbb`: what the brush and Fill paint. */\n foreground: string;\n /** `#rrggbb`: what the eraser paints on transparency-locked layers and masks. */\n background: string;\n /** Mixer Brush options (wet, load, mix, load / clean each stroke, RGB or pigment mixing). */\n mixer: MixerSettings;\n /** What the Mixer Brush carries now: its reservoir, how much is left and the picked-up colour. */\n mixerLoad: MixerLoad;\n}\n\n/** Paint tools a stroke can use. */\nexport type StrokeTool = 'brush' | 'eraser' | 'mixer';\n\n/** A stroke: tool, target (default the active layer and target) and brush / colour overrides. */\nexport interface StrokeInit {\n tool?: StrokeTool;\n layerId?: string;\n channel?: LayerTarget;\n /** Overrides of the paint options' brush. */\n brush?: Partial<BrushSettings>;\n /**\n * Paint colour (default the foreground). Masks take its grey value. For the mixer, the colour\n * the reservoir is loaded with: given, it loads the brush whatever `loadEachStroke` says.\n */\n color?: string | number[];\n /** Overrides of the paint options' Mixer Brush settings (tool `mixer`). */\n mixer?: Partial<MixerSettings>;\n /** Eraser colour on locked transparency / masks (default the background). */\n background?: string | number[];\n /** Kept with the stroke for replays (v1 brushes have no jitter). */\n seed?: number;\n}\n\nexport interface FillOptions {\n layerId?: string;\n channel?: LayerTarget;\n /** Default the foreground colour. */\n color?: string | number[];\n /** 0..1 (default 1). */\n opacity?: number;\n blend?: BlendMode;\n /** Limit to the selection when there is one (default true). */\n useSelection?: boolean;\n}\n\n/** What a stroke, fill or clear changed. */\nexport interface PaintStrokeResult {\n layerId: string;\n channel: LayerTarget;\n /** Document rectangle that changed (null when nothing did). */\n bounds: CropBox | null;\n dabs: number;\n /** The Mixer Brush's load after the stroke (tool `mixer` only). */\n mixerLoad?: MixerLoad;\n}\n\n/** An opaque pre-stroke history snapshot. */\nexport type HistoryEntryHandle = { readonly __historyEntry: unique symbol };\n\n/** Composites `coverage` into the tiles of `base` under `rects` (document space), returning the new surface. */\nfunction paintCoverageInto(base: TiledSurface, coverage: CoverageMap, apply: StrokeApply, rects: CropBox[]): TiledSurface {\n const tiles = new Map(base.tiles);\n const done = new Set<TileKey>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n if (done.has(key)) continue;\n done.add(key);\n const src = base.tiles.get(key)?.data ?? defaultTile(base);\n const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);\n const tile = finishTile(data, base.channels, base.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n }\n return new TiledSurfaceClass(base.channels, tiles, base.defaultValue);\n}\n\nfunction defaultTile(s: TiledSurface): Uint8ClampedArray {\n const d = new Uint8ClampedArray(TILE * TILE * s.channels);\n if (s.defaultValue) d.fill(s.defaultValue);\n return d;\n}\n\n/** How a Mixer Brush stroke starts (see `PaintStrokeSession`). */\nexport interface MixerStrokeInit {\n /** Makes the stroke over the session's live pixels. */\n create(pixels: MixerTarget): MixerStroke;\n /** Stores the brush load when the stroke ends and returns it (after any clean). */\n end(load: MixerLoad): MixerLoad;\n}\n\n/**\n * A stroke in progress (see `beginStroke`): `push` input points as they arrive, then `end` (one\n * undo step) or `cancel`. Brush and eraser strokes build up a coverage map composited over the\n * pre-stroke pixels; Mixer Brush strokes paint dab by dab into the live pixels, so each dab picks\n * up what the dabs before it laid down.\n */\nexport class PaintStrokeSession {\n private readonly before: HistoryEntryHandle;\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private readonly stroke: StrokeCoverage | null;\n private readonly mixer: MixerStroke | null;\n private readonly mixerInit: MixerStrokeInit | null = null;\n /** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */\n private readonly fresh = new Set<TileKey>();\n private last: StrokePoint | null = null;\n private closed = false;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly target: { id: string; channel: LayerTarget; surface: TiledSurface },\n stroke: StrokeCoverage | MixerStrokeInit,\n private readonly apply: StrokeApply\n ) {\n this.before = controller._captureForStroke();\n this.base = target.surface;\n this.live = new Map(this.base.tiles);\n if (stroke instanceof StrokeCoverage) {\n this.stroke = stroke;\n this.mixer = null;\n } else {\n this.stroke = null;\n this.mixer = stroke.create(this.mixerPixels());\n this.mixerInit = stroke;\n }\n }\n\n /** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */\n private mixerPixels(): MixerTarget {\n const a = this.apply;\n return {\n offset: a.offset,\n lockTransparency: a.lockTransparency,\n selection: a.selection,\n readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? (this.base.defaultValue ? defaultTile(this.base) : null),\n writeTile: (tx, ty) => {\n const key = tileKey(tx, ty);\n if (!this.touched.has(key)) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n this.live.set(key, { data: new Uint8ClampedArray(src), uniform: null });\n this.touched.add(key);\n }\n this.fresh.add(key);\n return this.live.get(key)!.data;\n },\n };\n }\n\n get layerId(): string {\n return this.target.id;\n }\n\n /** Adds input points (document pixels); the stack shows the stroke so far. */\n push(points: readonly StrokePoint[]): void {\n if (this.closed || !points.length) return;\n this.last = points[points.length - 1];\n this.update(this.mixer ? this.mixer.push(points) : this.stroke!.push(points));\n }\n\n private update(rects: CropBox[]): void {\n if (!rects.length) return;\n const ch = this.base.channels;\n if (this.mixer) {\n for (const key of this.fresh) this.live.set(key, { data: this.live.get(key)!.data, uniform: null });\n this.fresh.clear();\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n return;\n }\n const stroke = this.stroke!;\n // Per target tile, the union of the new dabs inside it: only that part is recomputed.\n const regions = new Map<TileKey, { tx: number; ty: number; r: CropBox }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n const cur = regions.get(key);\n regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r)! : r });\n }\n }\n for (const [key, { tx, ty, r }] of regions) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n const prev = this.touched.has(key) ? this.live.get(key)?.data : undefined;\n this.touched.add(key);\n const data = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Finishes the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new LayerOperationError('This stroke has already ended.');\n const last = this.last ?? undefined;\n this.update(this.mixer ? this.mixer.finish(last) : this.stroke!.finish(last));\n this.closed = true;\n const ch = this.base.channels;\n const tiles = new Map(this.base.tiles);\n let changed = false;\n for (const key of this.touched) {\n const live = this.live.get(key)!;\n const tile = finishTile(live.data, ch, this.base.defaultValue);\n const prev = this.base.tiles.get(key);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n if (!sameTile(prev, tile)) changed = true;\n }\n const surface = changed ? new TiledSurfaceClass(ch, tiles, this.base.defaultValue) : null;\n const mixerLoad = this.mixer ? this.mixerInit!.end(this.mixer.load) : undefined;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);\n const b = this.mixer ? this.mixer.bounds : this.stroke!.coverage.bounds;\n const result: PaintStrokeResult = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke!).dabCount };\n if (mixerLoad) result.mixerLoad = mixerLoad;\n return result;\n }\n\n /** Abandons the stroke and restores the layer. */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);\n }\n}\n\nfunction sameTile(a: Tile | undefined, b: Tile | null): boolean {\n if (!a || !b) return !a && !b;\n if (a === b) return true;\n if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;\n for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;\n return true;\n}\n\n/** Where a new annotation goes. */\nexport interface AnnotationPlacement {\n /** Put the annotation layer directly above this layer (default: the top of the stack). */\n aboveId?: string;\n}\n\n/** The image a history entry was recorded against. */\ninterface SourceSnapshot {\n sourceUrl: string | null;\n element: CanvasImageSource | null;\n dimensions: ImageDimensions;\n}\n\n/**\n * Undo/redo entry: the document as immutable objects. Layers, surfaces, tiles and selections are\n * shared with the other entries and the current state, so an entry costs about the tiles it changed.\n */\ninterface HistoryEntry {\n layers: ImageLayer[];\n activeLayerId: string | null;\n activeTarget: LayerTarget;\n crop: CropBox | null;\n transform: ImageTransform;\n outputOpacity: number;\n legacyOutputBlur?: number;\n globalLight?: GlobalLight;\n patterns?: EffectPattern[];\n selection: SelectionMask | null;\n source: SourceSnapshot;\n}\n\nexport const DEFAULT_ADJUSTMENTS: ImageAdjustments = DEFAULT_ADJUSTMENTS_VALUES;\n\n/**\n * Fills in defaults for stored adjustments. A stored object without `process` was saved before\n * process versions existed and keeps the legacy rendering.\n */\nexport function normalizeStoredAdjustments(stored: Partial<ImageAdjustments> | undefined | null): ImageAdjustments {\n if (!stored) return { ...DEFAULT_ADJUSTMENTS };\n const out: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS,\n ...stored,\n process: normalizeProcessVersion(stored.process),\n };\n // Layers are deep-copied and cleaned; an empty or missing list is left out entirely.\n const layers = normalizeAdjustmentLayers(stored.layers);\n if (layers.length) out.layers = layers;\n else delete out.layers;\n return out;\n}\n\nconst isLayered = (data: { version?: unknown } | null | undefined): data is SerializedLayeredImageEditorState =>\n Boolean(data && data.version === LAYERED_SCHEMA_VERSION && Array.isArray((data as { layers?: unknown }).layers));\n\n/**\n * Brings a stored editor state of any schema version to a clean state of the same family:\n * version 1 (and a missing version) has no adjustment layers, version 2 has them, and both stay\n * in the document-level schema so old states load and render exactly as before. Version 3\n * (layers) is returned with its document fields cleaned. To lift a v1 / v2 state into layers,\n * use {@link migrateToLayeredState}.\n */\nexport function migrateSerializedImageEditorState(\n data: Partial<SerializedImageEditorState> & { version?: number }\n): SerializedImageEditorState {\n if (isLayered(data)) {\n const out: SerializedLayeredImageEditorState = {\n ...data,\n crop: data.crop ? { ...data.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...data.transform },\n opacity: typeof data.opacity === 'number' ? Math.min(1, Math.max(0, data.opacity)) : 1,\n assets: { ...(data.assets ?? {}) },\n activeLayerId: typeof data.activeLayerId === 'string' ? data.activeLayerId : null,\n };\n return out;\n }\n const legacy = data as Partial<LegacySerializedImageEditorState>;\n const adjustments = normalizeStoredAdjustments(legacy.adjustments);\n return {\n version: adjustments.layers?.length ? 2 : 1,\n crop: legacy.crop ? { ...legacy.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...legacy.transform },\n adjustments: serializeAdjustments(adjustments),\n depthMask: {\n ...DEFAULT_DEPTH_MASK,\n ...legacy.depthMask,\n depthRange: [...(legacy.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange)] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(legacy.annotations ?? [])),\n ...(legacy.selection ? { selection: { ...legacy.selection } } : {}),\n };\n}\n\n/**\n * Lifts a stored state of any version into the layered schema (v3) for a canvas of\n * `width × height`: the source becomes the Background layer with the base sliders as its\n * `develop` filter and the depth mask as its parametric mask, adjustment layers go above it and\n * annotations on top (see `layersFromLegacy`).\n */\nexport function migrateToLayeredState(\n data: Partial<SerializedImageEditorState> & { version?: number },\n size: ImageDimensions\n): SerializedLayeredImageEditorState {\n const clean = migrateSerializedImageEditorState(data);\n if (isLayered(clean)) return clean;\n const stack = layersFromLegacy(clean, Math.round(size.width), Math.round(size.height));\n const { layers, assets } = serializeLayers(stack.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width: Math.round(size.width),\n height: Math.round(size.height),\n layers,\n assets,\n activeLayerId: null,\n crop: clean.crop,\n transform: clean.transform,\n opacity: stack.outputOpacity,\n };\n if (stack.legacyOutputBlur !== undefined) out.legacyOutputBlur = stack.legacyOutputBlur;\n if (clean.selection) out.selection = clean.selection;\n return out;\n}\n\nexport const DEFAULT_TRANSFORM: ImageTransform = DEFAULT_TRANSFORM_VALUES;\n\nexport const DEFAULT_DEPTH_MASK: DepthMaskOptions = DEFAULT_DEPTH_MASK_VALUES;\n\nconst DEVELOP_KEYS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/**\n * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,\n * tool interaction states, undo/redo history, serialization, and image export.\n */\nexport class ImageEditorController {\n private state: ImageEditorState;\n private renderer: ImageEditorRenderer;\n private imageElement: CanvasImageSource | null = null;\n private listeners = new Set<(state: ImageEditorState) => void>();\n private histogramListeners = new Set<(histogram: ImageHistogram | null) => void>();\n private onStateChange?: (state: ImageEditorState) => void;\n\n // History stacks\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private maxHistoryDepth: number;\n private maxHistoryBytes: number;\n private isApplyingHistory = false;\n private initialSnapshot: HistoryEntry;\n /** Pixels the selection tools sample (host-provided or read back from the source). */\n private samplePixels: PixelBuffer | null = null;\n private samplePixelsOverride = false;\n /** Decoded source pixels (the Background's surface), by the buffer or element they came from. */\n private sourceSurfaces = new WeakMap<object, PixelSurface>();\n private viewKey: unknown[] = [];\n /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */\n private compositor = new LayerCompositor();\n\n constructor(options: ImageEditorOptions = {}) {\n this.maxHistoryDepth = options.maxHistoryDepth ?? 50;\n this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;\n this.onStateChange = options.onStateChange;\n this.renderer = new ImageEditorRenderer();\n this.renderer.onPreviewHistogram = (histogram) => {\n this.histogramListeners.forEach((l) => l(histogram));\n };\n this.renderer.layerSources = {\n surfaceOf: (layer) => this.surfaceOf(layer),\n sourceIsOpaque: () => this.sourceIsOpaque(),\n version: () => `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: () => this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n\n this.state = {\n sourceUrl: typeof options.image === 'string' ? options.image : null,\n imageDimensions: { width: 800, height: 600 },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM },\n adjustments: { ...DEFAULT_ADJUSTMENTS },\n depthMask: { ...DEFAULT_DEPTH_MASK },\n annotations: [],\n selectedAnnotationId: null,\n activeTool: 'select',\n zoom: 1,\n pan: { x: 0, y: 0 },\n isOriginalCompared: false,\n selection: null,\n quickMask: false,\n layers: [],\n activeLayerId: null,\n activeTarget: 'pixels',\n outputOpacity: 1,\n paint: { brush: { ...DEFAULT_BRUSH }, foreground: '#000000', background: '#ffffff', mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },\n fit: options.fit ?? 'margin',\n };\n\n if (options.image instanceof Object && 'width' in options.image) {\n this.imageElement = options.image as CanvasImageSource;\n this.state.sourceUrl = (options.image as any).src || null;\n this.state.imageDimensions = {\n width: (options.image as any).naturalWidth || (options.image as any).width || 800,\n height: (options.image as any).naturalHeight || (options.image as any).height || 600,\n };\n }\n\n this.applyStored(options.initialState ?? {});\n if (options.initialState?.selection) {\n try {\n this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));\n } catch {\n // A damaged selection is dropped; the rest of the state still loads.\n }\n }\n\n this.initialSnapshot = this.captureHistoryEntry();\n\n if (typeof options.image === 'string') {\n this.loadImage(options.image);\n }\n }\n\n // --- Image Loading ---\n\n /**\n * Loads a new source image and makes it the baseline that `reset()` returns to.\n * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`\n * for undoable source replacement.\n */\n public async loadImage(src: string | HTMLImageElement): Promise<void> {\n // Keep the URL visible synchronously (UI bindings compare against it while loading).\n this.abandonContents();\n if (typeof src === 'string') this.state.sourceUrl = src;\n const source = await this.resolveSource(src);\n const prev = this.state.imageDimensions;\n this.imageElement = source.element;\n this.state.sourceUrl = source.sourceUrl;\n this.state.imageDimensions = source.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (prev.width !== source.dimensions.width || prev.height !== source.dimensions.height) {\n // Masks lifted from v1 / v2 documents follow the image, as they did when they were resampled at render.\n this.state.layers = resizeLegacyMasks(this.state.layers, prev, source.dimensions);\n }\n this.dropMismatchedSelection();\n\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n }\n\n private async resolveSource(src: string | HTMLImageElement): Promise<SourceSnapshot> {\n if (typeof src === 'string') {\n try {\n const img = await this.renderer.imageCache.get(src);\n return {\n sourceUrl: src,\n element: img,\n dimensions: {\n width: img.naturalWidth || img.width || 800,\n height: img.naturalHeight || img.height || 600,\n },\n };\n } catch {\n // In SSR / non-DOM environments, retain current dimensions\n return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };\n }\n }\n return {\n sourceUrl: (src as any).src || null,\n element: src,\n dimensions: {\n width: src.naturalWidth || src.width || 800,\n height: src.naturalHeight || src.height || 600,\n },\n };\n }\n\n public getImageElement(): CanvasImageSource | null {\n return this.imageElement;\n }\n\n // --- State & Subscription ---\n\n public getState(): ImageEditorState {\n this.syncViews();\n return this.state;\n }\n\n public subscribe(listener: (state: ImageEditorState) => void): () => void {\n this.syncViews();\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Where layer pixels changed since the last viewport render, when only strokes changed them\n * (the renderer then recomposites just those rectangles); `'all'` after any other change.\n */\n private pendingDirty: CropBox[] | 'all' = 'all';\n\n private notify(strokeRects?: CropBox[]): void {\n if (strokeRects && this.pendingDirty !== 'all') this.pendingDirty.push(...strokeRects);\n else if (!strokeRects) this.pendingDirty = 'all';\n this.syncViews();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n /** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */\n private syncViews(): void {\n const s = this.state;\n const key = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];\n if (key.length === this.viewKey.length && key.every((v, i) => v === this.viewKey[i])) return;\n this.viewKey = key;\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, s.activeLayerId);\n s.adjustments = view.adjustments;\n s.depthMask = view.depthMask;\n s.annotations = view.annotations;\n }\n\n private stack(): LayerStack {\n const out: LayerStack = { layers: this.state.layers, outputOpacity: this.state.outputOpacity };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n\n // --- History (Undo / Redo / Reset) ---\n\n private captureHistoryEntry(): HistoryEntry {\n const s = this.state;\n const entry: HistoryEntry = {\n layers: s.layers,\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n crop: s.crop ? { ...s.crop } : null,\n transform: { ...s.transform },\n outputOpacity: s.outputOpacity,\n selection: s.selection ?? null,\n source: {\n sourceUrl: s.sourceUrl,\n element: this.imageElement,\n dimensions: { ...s.imageDimensions },\n },\n };\n if (s.legacyOutputBlur !== undefined) entry.legacyOutputBlur = s.legacyOutputBlur;\n if (s.globalLight) entry.globalLight = { ...s.globalLight };\n if (s.patterns) entry.patterns = s.patterns;\n return entry;\n }\n\n private restoreHistoryEntry(entry: HistoryEntry): void {\n const s = this.state;\n s.layers = entry.layers;\n s.activeLayerId = entry.activeLayerId;\n s.activeTarget = entry.activeTarget;\n s.crop = entry.crop ? { ...entry.crop } : null;\n s.transform = { ...entry.transform };\n s.outputOpacity = entry.outputOpacity;\n if (entry.legacyOutputBlur !== undefined) s.legacyOutputBlur = entry.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n if (entry.globalLight) s.globalLight = { ...entry.globalLight };\n else delete s.globalLight;\n if (entry.patterns) s.patterns = entry.patterns;\n else delete s.patterns;\n s.selection = entry.selection;\n if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;\n this.imageElement = entry.source.element;\n s.sourceUrl = entry.source.sourceUrl;\n s.imageDimensions = { ...entry.source.dimensions };\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) {\n s.selectedAnnotationId = null;\n }\n }\n\n private recordHistory(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(this.captureHistoryEntry());\n if (this.undoStack.length > this.maxHistoryDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n\n /**\n * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest\n * undo steps are dropped while it exceeds `maxHistoryBytes`.\n */\n public get historyBytes(): number {\n const live = new Set<Tile>();\n collectStateTiles(this.state.layers, live);\n const held = new Set<Tile>();\n for (const e of [...this.undoStack, ...this.redoStack]) collectStateTiles(e.layers, held);\n let n = 0;\n for (const t of held) if (!live.has(t)) n += t.data.byteLength;\n return n;\n }\n\n private trimHistoryBytes(): void {\n if (!(this.maxHistoryBytes > 0) || this.undoStack.length <= 1) return;\n while (this.undoStack.length > 1 && this.historyBytes > this.maxHistoryBytes) this.undoStack.shift();\n }\n\n public get canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n public get canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n public undo(): boolean {\n if (!this.canUndo) return false;\n this.isApplyingHistory = true;\n this.redoStack.push(this.captureHistoryEntry());\n\n const prev = this.undoStack.pop()!;\n this.restoreHistoryEntry(prev);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public redo(): boolean {\n if (!this.canRedo) return false;\n this.isApplyingHistory = true;\n this.undoStack.push(this.captureHistoryEntry());\n\n const next = this.redoStack.pop()!;\n this.restoreHistoryEntry(next);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public reset(): void {\n this.recordHistory();\n this.restoreHistoryEntry(this.initialSnapshot);\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.state.activeTool = 'select';\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n // --- Active Tool ---\n\n public setTool(tool: ImageEditorTool): void {\n if (this.state.activeTool === tool) return;\n this.state.activeTool = tool;\n if (tool !== 'select') {\n this.state.selectedAnnotationId = null;\n }\n // If switching to crop tool and no crop is defined, initialize crop to full image\n if (tool === 'crop' && !this.state.crop) {\n this.state.crop = {\n x: 0,\n y: 0,\n width: this.state.imageDimensions.width,\n height: this.state.imageDimensions.height,\n };\n }\n this.notify();\n }\n\n // --- Crop ---\n\n public setCrop(crop: CropBox | null, addToHistory = true): void {\n if (addToHistory) {\n this.recordHistory();\n }\n this.state.crop = crop;\n this.notify();\n }\n\n public applyCropPreset(ratio: AspectRatio): void {\n this.recordHistory();\n const { width, height } = this.state.imageDimensions;\n // `ratio` is the on-screen aspect; the crop box lives in natural image space.\n if (ratio && isQuarterTurnSwapped(this.state.transform)) {\n ratio = 1 / ratio;\n }\n if (!ratio) {\n // Freeform: full image\n this.state.crop = { x: 0, y: 0, width, height };\n } else {\n let targetW = width;\n let targetH = targetW / ratio;\n\n if (targetH > height) {\n targetH = height;\n targetW = targetH * ratio;\n }\n\n const x = Math.max(0, Math.round((width - targetW) / 2));\n const y = Math.max(0, Math.round((height - targetH) / 2));\n this.state.crop = {\n x,\n y,\n width: Math.round(targetW),\n height: Math.round(targetH),\n };\n }\n this.notify();\n }\n\n public resetCrop(): void {\n this.recordHistory();\n this.state.crop = null;\n this.notify();\n }\n\n // --- Transformations (Rotate / Flip) ---\n\n public rotate(degrees: number): void {\n this.recordHistory();\n const current = this.state.transform.rotate;\n this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };\n this.notify();\n }\n\n public setRotation(degrees: number): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, rotate: degrees % 360 };\n this.notify();\n }\n\n public flipHorizontal(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };\n this.notify();\n }\n\n public flipVertical(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };\n this.notify();\n }\n\n // --- Layers (read) ---\n\n /** The layer stack, bottom to top (immutable objects). */\n public getLayers(): ImageLayer[] {\n return this.state.layers;\n }\n\n public getLayer(id: string): ImageLayer | undefined {\n return findLayer(this.state.layers, id)?.layer;\n }\n\n /** The active layer, or `undefined`. */\n public getActiveLayer(): ImageLayer | undefined {\n return this.state.activeLayerId ? this.getLayer(this.state.activeLayerId) : undefined;\n }\n\n /**\n * Makes a layer the active one (or none) and chooses whether painting targets its pixels or\n * its mask. View state: no undo step. Returns false for an unknown id.\n */\n public setActiveLayer(id: string | null, target: LayerTarget = 'pixels'): boolean {\n if (id !== null && !this.getLayer(id)) return false;\n const layer = id ? this.getLayer(id) : undefined;\n const t: LayerTarget = target === 'mask' && layer?.mask ? 'mask' : target === 'filter-mask' && layer?.type === 'smart' && layer.smartFilters?.mask ? 'filter-mask' : 'pixels';\n if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;\n this.state.activeLayerId = id;\n this.state.activeTarget = t;\n this.notify();\n return true;\n }\n\n /**\n * The pixels of a pixel layer, in document space (`x`, `y` is the top-left of the returned\n * buffer), or `null` for an unknown or empty layer. Resolves the Background's source pixels.\n */\n public getLayerPixels(id: string): (PixelBuffer & { x: number; y: number }) | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n const b = contentBounds(surface);\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height, data: readRegion(surface, b) };\n }\n\n private boundsCache = new WeakMap<object, CropBox | null>();\n\n /**\n * Document bounds of a layer's non-transparent pixels (pixel layers; the Background's source\n * included), or `null` for an empty or non-pixel layer.\n */\n public getLayerBounds(id: string): CropBox | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n let b = this.boundsCache.get(surface);\n if (b === undefined) {\n b = contentBounds(surface);\n this.boundsCache.set(surface, b);\n }\n return b ? { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height } : null;\n }\n\n /**\n * Composites the layer stack in document space without a canvas (annotation layers need one\n * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and\n * output opacity are not applied. With `layerIds`, only those layers are composited.\n */\n public compositePixels(rect?: CropBox, options: { layerIds?: string[] } = {}): PixelBuffer {\n const { width, height } = this.naturalSize;\n const r = rect ?? { x: 0, y: 0, width, height };\n let layers = this.state.layers;\n if (options.layerIds) {\n const keep = new Set(options.layerIds);\n layers = flattenLayers(layers).filter((l) => keep.has(l.id) && l.type !== 'group');\n }\n const data = this.compositor.composite(layers, r, this.compositorSources());\n return { width: r.width, height: r.height, data };\n }\n\n private compositorSources(): CompositorSources {\n const { width, height } = this.naturalSize;\n return {\n width,\n height,\n surfaceOf: (l) => this.surfaceOf(l),\n version: `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n }\n\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 1;\n\n /** Changes whenever the Background's source pixels can have changed. */\n private sourceVersion(): string {\n const key = (this.samplePixelsOverride ? this.samplePixels : this.imageElement ?? this.samplePixels) as object | null;\n if (!key) return '0';\n let id = this.sourceIds.get(key);\n if (!id) {\n id = this.nextSourceId++;\n this.sourceIds.set(key, id);\n }\n return String(id);\n }\n\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer | SmartObjectLayer): PixelSurface {\n if (layer.type === 'smart' || layer.source !== 'document') return layer.surface;\n return this.sourceSurface() ?? layer.surface;\n }\n\n /**\n * The source image as a surface (cached): host-supplied sample pixels first, then the decoded\n * element, then pixels read back for the selection tools.\n */\n private sourceSurface(): PixelSurface | null {\n const { width, height } = this.naturalSize;\n const fromPixels = (px: PixelBuffer) => {\n let s = this.sourceSurfaces.get(px as object);\n if (!s) {\n s = surfaceFromPixels(px, 4);\n this.sourceSurfaces.set(px as object, s);\n }\n return s;\n };\n const px = this.samplePixels;\n const fits = Boolean(px && px.width === width && px.height === height);\n if (this.samplePixelsOverride && fits) return fromPixels(px!);\n const el = this.imageElement as unknown as object | null;\n if (el) {\n const cached = this.sourceSurfaces.get(el);\n if (cached) return cached;\n const decoded = this.decodeSource();\n if (decoded) {\n if (!this.samplePixelsOverride) this.samplePixels = decoded;\n const s = surfaceFromPixels(decoded, 4);\n this.sourceSurfaces.set(el, s);\n this.sourceSurfaces.set(decoded as object, s);\n return s;\n }\n }\n return fits ? fromPixels(px!) : null;\n }\n\n private opaqueCache = new WeakMap<object, boolean>();\n\n /** Whether every source pixel is opaque (some legacy renders depend on it). */\n private sourceIsOpaque(): boolean {\n const s = this.sourceSurface();\n if (!s) return true;\n let v = this.opaqueCache.get(s);\n if (v === undefined) {\n const { width, height } = this.naturalSize;\n v = isOpaqueInside(s, width, height);\n this.opaqueCache.set(s, v);\n }\n return v;\n }\n\n /** Synchronously reads the source element's pixels through a canvas, or `null`. */\n private decodeSource(): PixelBuffer | null {\n const el = this.imageElement;\n if (!el) return null;\n const { width, height } = this.naturalSize;\n try {\n const canvas = this.renderer.createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n ctx.drawImage(el, 0, 0, width, height);\n const data = ctx.getImageData(0, 0, width, height);\n if (!data?.data || data.data.length < width * height * 4) return null;\n return { width, height, data: data.data };\n } catch {\n return null;\n }\n }\n\n // --- Layer operations ---\n\n /** Records one undo step and applies a new stack (and any other state change in `extra`). */\n private commitLayers(next: ImageLayer[], extra?: () => void, commitToHistory = true): void {\n if (commitToHistory) this.recordHistory();\n this.state.layers = next;\n extra?.();\n if (this.state.activeLayerId && !findLayer(next, this.state.activeLayerId)) {\n this.state.activeLayerId = null;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n }\n\n /** The default position of a new layer: above the active layer, else the top of the stack. */\n private newLayerPlacement(placement: LayerPlacement): LayerPlacement {\n if (placement.aboveId || placement.belowId || placement.parentId || placement.index !== undefined) return placement;\n const active = this.state.activeLayerId ? findLayer(this.state.layers, this.state.activeLayerId) : null;\n return active ? { aboveId: active.layer.id } : {};\n }\n\n /**\n * Adds a pixel layer (one undo step) and makes it active: empty, filled with `color` over the\n * canvas, or holding `pixels` placed at `x`, `y`. Goes above the active layer unless placed.\n * Returns its id.\n */\n public addPixelLayer(init: PixelLayerInit = {}): string {\n const { width, height } = this.naturalSize;\n let surface = emptySurface(4);\n if (init.pixels) {\n const px = init.pixels;\n if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {\n throw new LayerOperationError('pixels must be RGBA with width × height × 4 bytes.');\n }\n surface = surfaceFromPixels(px, 4);\n } else if (init.color !== undefined) {\n const c = parseColor(init.color);\n surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));\n }\n const blend = parseBlend(init.blend) as BlendMode | undefined;\n if (init.opacity !== undefined && !(init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer: PixelLayer = {\n type: 'pixel',\n id: init.id && !ids.has(init.id) ? init.id : newLayerId(),\n name: init.name?.trim() || this.nextLayerName(),\n visible: init.visible ?? true,\n opacity: init.opacity ?? 1,\n blend: blend ?? 'normal',\n surface,\n offset: { x: Math.round(init.x ?? 0), y: Math.round(init.y ?? 0) },\n };\n if (init.fillOpacity !== undefined && init.fillOpacity !== 1) layer.fillOpacity = init.fillOpacity;\n const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = layer.id;\n this.state.activeTarget = 'pixels';\n });\n return layer.id;\n }\n\n /**\n * `addPixelLayer` from an image: a PNG data URL or base64 PNG is decoded without a canvas;\n * other formats and URLs need a browser. Returns the new layer id.\n */\n public async addPixelLayerFromImage(image: string, init: Omit<PixelLayerInit, 'pixels' | 'color'> = {}): Promise<string> {\n const pixels = await this.decodeImage(image);\n return this.addPixelLayer({ ...init, pixels });\n }\n\n /** RGBA pixels of an image given as a data URL, base64 PNG or URL. */\n public async decodeImage(image: string): Promise<PixelBuffer> {\n const trimmed = image.trim();\n let png: Uint8Array | null = null;\n if (/^data:image\\/png/i.test(trimmed)) png = parseDataUrl(trimmed)?.bytes ?? null;\n else if (/^[A-Za-z0-9+/=\\s]+$/.test(trimmed) && trimmed.length > 16) {\n try {\n const bytes = base64ToBytes(trimmed);\n if (isPng(bytes)) png = bytes;\n } catch {\n png = null;\n }\n }\n if (png) {\n try {\n const d = decodePng(png);\n return { width: d.width, height: d.height, data: d.data };\n } catch {\n // Fall back to the browser decoder below (interlaced PNGs).\n }\n }\n const src = png ? `data:image/png;base64,${bytesToBase64(png)}` : trimmed;\n let img: HTMLImageElement;\n try {\n img = await this.renderer.imageCache.get(src);\n } catch {\n throw new LayerOperationError('The image could not be decoded here. Pass a PNG (data URL or base64), or run in a browser for other formats and URLs.');\n }\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const ctx = this.renderer.createCanvas(w, h).getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') throw new LayerOperationError('Decoding this image needs a canvas; pass a PNG instead.');\n ctx.drawImage(img, 0, 0, w, h);\n const data = ctx.getImageData(0, 0, w, h);\n return { width: w, height: h, data: data.data };\n }\n\n private nextLayerName(): string {\n const names = new Set(flattenLayers(this.state.layers).map((l) => l.name));\n let n = 1;\n while (names.has(`Layer ${n}`)) n++;\n return `Layer ${n}`;\n }\n\n /** Duplicates a layer (with its children) directly above it, as one undo step. Returns the copy's id. */\n public duplicateLayer(id: string, name?: string): string {\n const res = duplicateLayerOp(this.state.layers, id, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Deletes a layer (refused when locked), as one undo step. */\n public removeLayer(id: string): void {\n const next = removeLayerOp(this.state.layers, id);\n this.commitLayers(next, () => {\n if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === 'annotation' && l.annotation.id === this.state.selectedAnnotationId)) {\n this.state.selectedAnnotationId = null;\n }\n });\n }\n\n /** Moves a layer above / below another, or into a group, as one undo step. Returns its new parent and index. */\n public moveLayer(id: string, placement: LayerPlacement): { parentId: string | null; index: number } {\n const next = moveLayerOp(this.state.layers, id, placement);\n const loc = findLayer(next, id)!;\n if (next === this.state.layers) return { parentId: loc.parent?.id ?? null, index: loc.index };\n this.commitLayers(next);\n return { parentId: loc.parent?.id ?? null, index: loc.index };\n }\n\n /**\n * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as\n * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.\n */\n public setLayerProperties(id: string, props: LayerProperties, commitToHistory = true): ImageLayer {\n const next = setLayerPropertiesOp(this.state.layers, id, props);\n this.commitLayers(next, undefined, commitToHistory);\n return findLayer(next, id)!.layer;\n }\n\n /** Adds an empty pass-through group above the active layer (or at `placement`) and makes it active. Returns its id. */\n public addGroup(init: LayerPlacement & { name?: string; blend?: LayerBlendMode } = {}): string {\n const blend = init.blend === undefined ? 'pass-through' : (parseBlend(init.blend, true) as LayerBlendMode);\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name: init.name?.trim() || 'Group',\n visible: true,\n opacity: 1,\n blend,\n children: [],\n };\n const next = placeLayer(this.state.layers, group, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = group.id;\n this.state.activeTarget = 'pixels';\n });\n return group.id;\n }\n\n /** Puts sibling layers into a new group (one undo step). Returns the group id. */\n public groupLayers(ids: string[], name?: string): string {\n const res = groupLayersOp(this.state.layers, ids, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Replaces a group by its children (one undo step). Returns the child ids, bottom to top. */\n public ungroupLayer(id: string): string[] {\n const res = ungroupLayerOp(this.state.layers, id);\n this.commitLayers(res.layers);\n return res.ids;\n }\n\n /**\n * Moves a pixel layer to an absolute offset (`x`, `y`) or by `dx`, `dy`, as one undo step\n * (`commitToHistory = false` for the moves of a drag). Returns the offset and document bounds.\n */\n public offsetLayer(id: string, move: { x?: number; y?: number; dx?: number; dy?: number }, commitToHistory = true): { offset: { x: number; y: number }; bounds: CropBox | null } {\n const layer = requireLayer(this.state.layers, id).layer;\n if (layer.type === 'smart') {\n // A smart object moves through its placement: x, y is where its top-left corner goes.\n const tl = layer.placement.corners[0];\n const dx = move.x !== undefined ? move.x - tl.x : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - tl.y : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n this.commitLayers(offsetSmartObject(this.state.layers, id, dx, dy, this.canvasSize), undefined, commitToHistory);\n const moved = findLayer(this.state.layers, id)!.layer as SmartObjectLayer;\n return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };\n }\n const current = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const x = move.x ?? current.x + (move.dx ?? 0);\n const y = move.y ?? current.y + (move.dy ?? 0);\n const next = offsetLayerOp(this.state.layers, id, { x, y });\n this.commitLayers(next, undefined, commitToHistory);\n return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };\n }\n\n /**\n * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or\n * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint\n * target when the layer is active.\n */\n public addLayerMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' = 'reveal-all', props: LayerMaskProps = {}): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n let mask: RasterMask;\n if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n if (layer.type === 'pixel' && (layer.offset.x || layer.offset.y)) {\n // Masks live in the layer's space (they move with it).\n mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };\n }\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const next = setLayerMaskOp(this.state.layers, id, mask);\n this.commitLayers(next, () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'mask';\n });\n return mask;\n }\n\n /** Removes a layer's mask (one undo step). */\n public removeLayerMask(id: string): void {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, null), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */\n public setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory = true): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n const mask = withMaskProps(layer.mask, props);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, mask), undefined, commitToHistory);\n return mask;\n }\n\n /**\n * Bakes a pixel layer's mask into its pixels (alpha × coverage, including density, feather or\n * the depth map) and removes the mask, as one undo step.\n */\n public async applyLayerMask(id: string): Promise<void> {\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Only a pixel layer's mask can be applied to its pixels; \"${id}\" is a ${layer.type} layer. Remove the mask or keep it.`);\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n requireUnlocked(layer, 'pixels', 'apply its mask');\n const { width, height } = this.naturalSize;\n await this.renderer.prepareLayerAssets([layer], width, height);\n const sources = this.renderer.compositorSources(width, height);\n const surface = this.surfaceOf(layer);\n let out = surface;\n const b = surface.bounds;\n if (b) {\n const draft = new SurfaceDraft(surface);\n for (const t of tilesInRect(b)) {\n const data = draft.peek(t.tx, t.ty);\n if (!data) continue;\n const r = { x: t.tx * TILE + layer.offset.x, y: t.ty * TILE + layer.offset.y, width: TILE, height: TILE };\n const cov = this.compositor.maskCoverage(layer, r, sources);\n if (!cov) continue;\n const d = draft.tile(t.tx, t.ty);\n for (let p = 0, i = 3; p < cov.length; p++, i += 4) d[i] = Math.round(d[i] * cov[p]);\n }\n out = draft.commit();\n }\n const next: PixelLayer = { ...layer, surface: out };\n delete next.mask;\n delete next.source;\n this.commitLayers(mapLayer(this.state.layers, id, () => next), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /**\n * Layer via Copy (or via Cut): a new pixel layer with the selected pixels of a pixel layer (the\n * active one by default), directly above it, as one undo step. Returns the new id.\n */\n public layerViaCopy(options: { layerId?: string; cut?: boolean; name?: string } = {}): string {\n const id = options.layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError('No layer given and none is active; pass layerId (see list_layers).');\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer via copy needs a pixel layer; \"${id}\" is a ${layer.type} layer.`);\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select the pixels to copy first.');\n if (options.cut) requireUnlocked(layer, 'pixels', 'cut from it');\n const surface = this.surfaceOf(layer);\n const sb = sel.bounds;\n const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, src);\n const kept = options.cut ? new Uint8ClampedArray(px) : null;\n for (let y = 0; y < sb.height; y++) {\n for (let x = 0; x < sb.width; x++) {\n const c = sel.data[(sb.y + y) * sel.width + sb.x + x];\n const i = (y * sb.width + x) * 4 + 3;\n if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));\n px[i] = Math.round(px[i] * (c / 255));\n }\n }\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: options.name?.trim() || `${layer.name || 'Layer'} ${options.cut ? 'cut' : 'copy'}`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),\n offset: { ...layer.offset },\n };\n let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);\n if (kept) {\n const cutLayer: PixelLayer = { ...layer, surface: writeRegion(surface, src, kept) };\n delete cutLayer.source;\n next = mapLayer(next, id, () => cutLayer);\n }\n this.commitLayers(next, () => {\n this.state.activeLayerId = copy.id;\n this.state.activeTarget = 'pixels';\n });\n return copy.id;\n }\n\n // --- Free Transform ---\n\n private openTransform: FreeTransformSession | null = null;\n\n /** The Free Transform in progress (see `beginFreeTransform`), or `null`. */\n public getFreeTransform(): FreeTransformSession | null {\n if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;\n return this.openTransform;\n }\n\n private transformHost(): FreeTransformHost {\n return {\n layers: () => this.state.layers,\n activeLayerId: () => this.state.activeLayerId,\n activeTarget: () => this.state.activeTarget,\n selection: () => (this.state.selection?.bounds ? this.state.selection : null),\n canvas: () => this.naturalSize,\n pixelsOf: (layer) => this.surfaceOf(layer),\n capture: () => this.captureHistoryEntry(),\n show: (layers, selection, active) => {\n this.state.layers = layers;\n this.state.selection = selection;\n if (active !== this.state.activeLayerId) {\n this.state.activeLayerId = active;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n },\n record: (before) => {\n if (this.isApplyingHistory) return;\n this.undoStack.push(before as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n this.notify();\n },\n restore: (before) => {\n const e = before as HistoryEntry;\n this.state.layers = e.layers;\n this.state.selection = e.selection;\n this.state.activeLayerId = e.activeLayerId;\n this.state.activeTarget = e.activeTarget;\n this.notify();\n },\n };\n }\n\n /**\n * Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer\n * via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected\n * pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with\n * `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.\n * Refused while another transform is open.\n */\n public beginFreeTransform(init: FreeTransformInit = {}): FreeTransformSession {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is already open; commit or cancel it first.');\n const session = new FreeTransformSession(this.transformHost(), init);\n this.openTransform = session;\n this.notify();\n return session;\n }\n\n /**\n * Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners\n * from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:\n * distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and\n * commits it as one undo step. Returns what changed and the new bounds.\n */\n public transformLayer(spec: FreeTransformInit & { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams; warp?: WarpMesh | WarpPreset | null }): FreeTransformResult {\n const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== undefined).length;\n if (given > 1) throw new LayerOperationError('Give only one of matrix, corners and params.');\n if (!given && spec.warp === undefined) throw new LayerOperationError('Give matrix, corners or params (or a warp).');\n if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {\n throw new LayerOperationError('matrix must be six finite numbers [a, b, c, d, e, f] (x\\' = a·x + c·y + e, y\\' = b·x + d·y + f).');\n }\n if (spec.matrix) {\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the layer to a line); its determinant a·d − b·c must not be 0.');\n }\n const session = this.beginFreeTransform(spec);\n try {\n // Relative to where the content is now (a smart object's current placement).\n const g = session.startGeometry;\n const corners = spec.matrix ? (g.corners.map((p) => applyMatrix(spec.matrix!, p)) as unknown as Quad) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;\n let warp: WarpMesh | null = spec.warp === undefined ? (g.warp ?? null) : null;\n if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(session.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points, row by row.');\n warp = spec.warp;\n }\n }\n session.update({ corners, warp }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */\n public flipLayer(axis: 'horizontal' | 'vertical', init: FreeTransformInit = {}): FreeTransformResult {\n if (axis !== 'horizontal' && axis !== 'vertical') throw new LayerOperationError(`axis must be horizontal or vertical, got \"${String(axis)}\".`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n session.update({ corners: flipCorners(session.startGeometry, axis) }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /**\n * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's\n * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).\n */\n public rotateLayer90(turn: 90 | -90 | 180, init: FreeTransformInit = {}): FreeTransformResult {\n if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n // The frame in the document (a smart object's placed outline).\n const b = session.smartObject ? (transformedBox(session.startGeometry) ?? session.box) : session.box;\n // About the centre, rounded so an odd-by-even box still lands on the pixel grid.\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);\n const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);\n const m: AffineMatrix =\n turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];\n session.update({ corners: session.startGeometry.corners.map((p) => applyMatrix(m, p)) as unknown as Quad }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */\n public addLayerFilter(id: string, filter: Partial<LayerFilter> & { kind: LayerFilter['kind'] }): LayerFilter[] {\n const f = normalizeFilter(filter);\n const next = addLayerFilterOp(this.state.layers, id, f);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */\n public updateLayerFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): LayerFilter[] {\n const next = updateLayerFilterOp(this.state.layers, id, filterId, changes);\n this.commitLayers(next, undefined, commitToHistory);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Removes one filter (one undo step). Returns the remaining filters. */\n public removeLayerFilter(id: string, filterId: string): LayerFilter[] {\n const next = removeLayerFilterOp(this.state.layers, id, filterId);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n // --- Smart objects ---\n\n private get canvasSize(): { width: number; height: number } {\n return this.naturalSize;\n }\n\n /**\n * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the\n * active one) become one smart object at the topmost one's position. A single pixel layer keeps\n * its id and properties and its filters become smart filters (it renders exactly as before);\n * anything else nests as a layer document. Returns the smart object's id.\n */\n public convertToSmartObject(ids?: string[], options: { name?: string } = {}): string {\n const list = ids?.length ? ids : this.state.activeLayerId ? [this.state.activeLayerId] : [];\n if (!list.length) throw new LayerOperationError('No layers given and none is active; pass layerIds (see list_layers).');\n const res = convertToSmartObjectOp(this.state.layers, list, this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l), name: options.name });\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Layer › Smart Objects › Rasterize (one undo step): the smart object becomes a pixel layer holding what it renders. */\n public rasterizeSmartObject(id: string): string {\n const res = rasterizeSmartObjectOp(this.state.layers, id);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId === id && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n });\n return id;\n }\n\n /** A smart object's contents, placement and smart filters as plain data (see `SmartObjectInfo`). */\n public getSmartObject(id: string): SmartObjectInfo {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const g = placementGeometry(layer);\n const readout = describeTransform(g);\n const stack = layer.smartFilters;\n return {\n layerId: layer.id,\n contents: {\n id: c.id,\n name: c.name,\n kind: c.kind,\n width: c.width,\n height: c.height,\n decoded: isContentsDecoded(c),\n fileType: c.file?.type ?? null,\n linkedPath: c.linked?.path ?? null,\n instances: instancesOf(this.state.layers, c.id),\n layerCount: c.document ? flattenLayers(c.document.layers).length : 0,\n },\n placement: {\n corners: g.corners.map((p) => ({ x: p.x, y: p.y })) as unknown as Quad,\n warp: layer.placement.warp ?? null,\n interpolation: layer.placement.interpolation ?? 'bicubic',\n affine: readout.affine,\n scaleX: readout.scaleX,\n scaleY: readout.scaleY,\n rotate: readout.rotate,\n skewX: readout.skewX,\n center: anchorPoint(g, 'center'),\n bounds: transformedBox(g),\n },\n smartFilters: {\n enabled: stack?.enabled ?? true,\n filters: (stack?.filters ?? []).map((f) => (f.kind === 'psd' ? { id: f.id, kind: 'psd', name: f.name, enabled: f.enabled, blend: f.blend, opacity: f.opacity } : { ...f })),\n mask: stack?.mask\n ? { enabled: stack.mask.enabled !== false, linked: stack.mask.linked !== false, density: stack.mask.density ?? 1, feather: stack.mask.feather ?? 0, bounds: contentBounds(stack.mask.surface), defaultValue: stack.mask.defaultValue }\n : null,\n },\n bounds: this.getLayerBounds(id),\n };\n }\n\n /**\n * Sets a smart object's placement (one undo step, or none with `commitToHistory = false` during a\n * drag): where the contents' corners land, an optional warp in contents pixels and the\n * interpolation. The contents render again from the source; a linked mask follows a warp-free\n * change. Returns the new placement summary.\n */\n public setSmartObjectPlacement(id: string, placement: SmartPlacement, commitToHistory = true): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const res = setSmartPlacement(this.state.layers, id, placement, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return { ...this.getSmartObject(id).placement, masksMoved: res.masksMoved };\n }\n\n /**\n * Sets a smart object's transform absolutely (one undo step): exactly one of `corners` (where the\n * contents' corners land), `matrix` (contents pixels → document) or the numbers (`scaleX`,\n * `scaleY` in percent of the contents' own size, `rotate`, `skewX`, `skewY`, and `x`, `y` where\n * the contents' centre goes, default where it is now); plus, optionally, a warp (a preset, a mesh\n * in contents pixels, or `null` to remove it) and the interpolation. Unlike `transformLayer`,\n * nothing is relative to the current transform.\n */\n public setSmartObjectTransform(\n id: string,\n spec: {\n corners?: Quad;\n matrix?: AffineMatrix;\n scaleX?: number;\n scaleY?: number;\n rotate?: number;\n skewX?: number;\n skewY?: number;\n x?: number;\n y?: number;\n warp?: WarpMesh | WarpPreset | null;\n interpolation?: Interpolation;\n }\n ): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const layer = requireSmartObject(this.state.layers, id);\n const g = placementGeometry(layer);\n const numeric = (['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y'] as const).filter((k) => spec[k] !== undefined);\n const ways = [spec.corners !== undefined, spec.matrix !== undefined, numeric.length > 0].filter(Boolean).length;\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: corners, matrix, or the numbers (scaleX, scaleY, rotate, skewX, skewY, x, y).');\n let corners: Quad = g.corners;\n if (spec.corners) corners = spec.corners;\n else if (spec.matrix) {\n if (!(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) throw new LayerOperationError(\"matrix must be six finite numbers [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\");\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the contents to a line); a·d − b·c must not be 0.');\n corners = cornersFromMatrix(g.box, spec.matrix);\n } else if (numeric.length) {\n if (spec.scaleX === 0 || spec.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the contents to a line).');\n for (const k of numeric) if (!Number.isFinite(spec[k] as number)) throw new LayerOperationError(`${k} must be a finite number.`);\n const now = describeTransform(g);\n const centre = anchorPoint(g, 'center');\n const params = {\n scaleX: spec.scaleX ?? (now.affine ? now.scaleX : 100),\n scaleY: spec.scaleY ?? (now.affine ? now.scaleY : 100),\n rotate: spec.rotate ?? (now.affine ? now.rotate : 0),\n skewX: spec.skewX ?? (now.affine ? now.skewX : 0),\n skewY: spec.skewY ?? 0,\n };\n const m = matrixFromParams(g.box, { ...params, reference: 'center' });\n const cx = g.box.width / 2;\n const cy = g.box.height / 2;\n const to = { x: spec.x ?? centre.x, y: spec.y ?? centre.y };\n corners = cornersFromMatrix(g.box, multiplyMatrices(m, translateMatrix(to.x - cx, to.y - cy)));\n }\n let warp: WarpMesh | null | undefined = layer.placement.warp ?? null;\n if (spec.warp === null) warp = null;\n else if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(g.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points in contents pixels, row by row.');\n warp = spec.warp;\n }\n }\n const next: SmartPlacement = { corners, warp, interpolation: spec.interpolation ?? layer.placement.interpolation };\n return this.setSmartObjectPlacement(id, next);\n }\n\n /**\n * Layer › Smart Objects › Replace Contents (one undo step): new contents for the smart object and\n * every instance linked to it, from RGBA pixels, PSD bytes (a layer document) or contents. Each\n * instance keeps its scale and rotation about the contents' centre. Returns the contents id and\n * the layers that changed.\n */\n public replaceSmartObjectContents(\n id: string,\n source: { pixels: PixelBuffer; name?: string } | { psd: Uint8Array; name?: string } | { contents: SmartContents }\n ): { contentsId: string; width: number; height: number; layerIds: string[] } {\n const layer = requireSmartObject(this.state.layers, id);\n let contents: SmartContents;\n if ('contents' in source) contents = source.contents;\n else if ('pixels' in source) contents = rasterContents(source.pixels, source.name ?? layer.contents.name.replace(/\\.\\w+$/, '.png'));\n else {\n const psd = importPsd(source.psd, { newId: () => newLayerId() });\n contents = documentContents({ layers: psd.layers, globalLight: psd.globalLight, patterns: psd.patterns.length ? psd.patterns : undefined }, psd.width, psd.height, source.name ?? `${layer.name || 'Contents'}.psb`);\n contents.file = { bytes: source.psd.slice(), type: psd.info.psb ? 'psb' : 'psd' };\n }\n const res = replaceContentsOp(this.state.layers, layer.contents.id, contents, this.canvasSize);\n this.commitLayers(res.layers);\n return { contentsId: layer.contents.id, width: contents.width, height: contents.height, layerIds: res.ids };\n }\n\n /** `replaceSmartObjectContents` from an image (PNG data URL or base64 decodes anywhere; other formats need a browser). */\n public async replaceSmartObjectContentsFromImage(id: string, image: string, name?: string): Promise<{ contentsId: string; width: number; height: number; layerIds: string[] }> {\n const pixels = await this.decodeImage(image);\n return this.replaceSmartObjectContents(id, { pixels, name });\n }\n\n /**\n * Layer › Smart Objects › Export Contents: the file the contents came from while unedited, else a\n * PNG (raster contents) or a PSD / PSB of the nested document. Returns the file name, MIME type\n * and bytes.\n */\n public exportSmartObjectContents(id: string): { name: string; mimeType: string; bytes: Uint8Array } {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const base = c.name.replace(/\\.\\w+$/, '') || 'Contents';\n if (c.file) {\n const mime: Record<string, string> = { psd: 'image/vnd.adobe.photoshop', psb: 'image/vnd.adobe.photoshop', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', tif: 'image/tiff', tiff: 'image/tiff', gif: 'image/gif' };\n const ext = c.file.type.toLowerCase();\n return { name: /\\.\\w+$/.test(c.name) ? c.name : `${base}.${ext}`, mimeType: mime[ext] ?? 'application/octet-stream', bytes: c.file.bytes };\n }\n if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not available here.`);\n if (c.kind === 'raster') {\n const data = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });\n return { name: `${base}.png`, mimeType: 'image/png', bytes: encodePng(c.width, c.height, data) };\n }\n const doc = c.document!;\n const merged = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });\n const prepared = prepareNestedForPsd(doc.layers);\n const res = exportPsd({ width: c.width, height: c.height, layers: prepared, merged, patterns: doc.patterns, globalLight: doc.globalLight });\n return { name: `${base}.${res.psb ? 'psb' : 'psd'}`, mimeType: 'image/vnd.adobe.photoshop', bytes: res.bytes };\n }\n\n /**\n * Layer › Smart Objects › New Smart Object via Copy (`linked: false`: independent contents) or a\n * linked duplicate (`linked: true`: edits to the contents show in both), directly above, as one\n * undo step. Returns the copy's id.\n */\n public copySmartObject(id: string, linked = false, name?: string): string {\n const res = copySmartObjectOp(this.state.layers, id, linked, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */\n public addSmartFilter(id: string, filter: Record<string, unknown> & { kind: string }, index?: number): SmartFilter {\n const res = addSmartFilterOp(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);\n this.commitLayers(res.layers);\n return res.filter;\n }\n\n /** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */\n public updateSmartFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): SmartFilter {\n const res = updateSmartFilterOp(this.state.layers, id, filterId, changes, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return res.filter;\n }\n\n /** Removes one smart filter (one undo step). Returns the remaining filters. */\n public removeSmartFilter(id: string, filterId: string): SmartFilter[] {\n const res = removeSmartFilterOp(this.state.layers, id, filterId, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.filters ?? [];\n }\n\n /** Moves one smart filter to `index` (0 = bottom, applied first), as one undo step. Returns the filters. */\n public moveSmartFilter(id: string, filterId: string, index: number): SmartFilter[] {\n const res = moveSmartFilterOp(this.state.layers, id, filterId, index, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.filters ?? [];\n }\n\n /** Turns a smart object's whole filter stack on or off (one undo step). */\n public setSmartFiltersEnabled(id: string, enabled: boolean): boolean {\n const res = setSmartFiltersEnabledOp(this.state.layers, id, enabled, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.enabled ?? true;\n }\n\n /**\n * Sets the smart filter mask (one undo step): `reveal-all`, `hide-all`, `selection` (the\n * selection's coverage), or `none` to remove it; `props` sets density, feather, enabled and linked.\n * Painting it targets channel `filter-mask`. Returns the mask, or `null` when removed.\n */\n public setSmartFilterMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props: LayerMaskProps = {}, commitToHistory = true): RasterMask | null {\n const layer = requireSmartObject(this.state.layers, id);\n let mask: RasterMask | null;\n if (from === 'none') mask = null;\n else if (from === 'keep') {\n if (!layer.smartFilters?.mask) throw new LayerOperationError(`Smart object \"${layer.name || id}\" has no filter mask; add one with from reveal-all, hide-all or selection.`);\n mask = withMaskProps(layer.smartFilters.mask, props);\n } else if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const res = setSmartFilterMaskOp(this.state.layers, id, mask, this.canvasSize);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId !== id) return;\n if (mask && from !== 'keep') this.state.activeTarget = 'filter-mask';\n else if (!mask && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n }, commitToHistory);\n return res.layer.smartFilters?.mask ?? null;\n }\n\n /**\n * Decodes smart object contents that came as files the synchronous decoders cannot read (a JPEG\n * or TIFF embedded in a PSD), through the browser's image decoder. No undo step: the pixels on\n * screen do not change. Returns the contents ids still undecoded.\n */\n public async prepareSmartObjects(): Promise<string[]> {\n const pending = new Map<string, SmartContents>();\n for (const l of flattenLayers(this.state.layers)) if (l.type === 'smart' && !isContentsDecoded(l.contents) && l.contents.file) pending.set(l.contents.id, l.contents);\n const left: string[] = [];\n for (const [cid, c] of pending) {\n try {\n const url = `data:${c.file!.type === 'png' ? 'image/png' : c.file!.type === 'gif' ? 'image/gif' : c.file!.type === 'tif' || c.file!.type === 'tiff' ? 'image/tiff' : 'image/jpeg'};base64,${bytesToBase64(c.file!.bytes)}`;\n const px = await this.decodeImage(url);\n const decoded: SmartContents = { ...c, kind: 'raster', raster: rasterContents(px, c.name, cid).raster, width: px.width, height: px.height };\n const swap = (layers: ImageLayer[]) => {\n let out = layers;\n for (const l of flattenLayers(layers)) if (l.type === 'smart' && l.contents.id === cid) out = mapLayer(out, l.id, (x) => ({ ...(x as SmartObjectLayer), contents: decoded }));\n return out;\n };\n this.state.layers = swap(this.state.layers);\n for (const e of [...this.undoStack, ...this.redoStack]) e.layers = swap(e.layers);\n } catch {\n left.push(cid);\n }\n }\n if (pending.size) this.notify();\n return left;\n }\n\n // --- Smart object contents (Edit Contents) ---\n\n private contentsStack: Array<{ layerId: string; contentsId: string; name: string; saved: SavedDocument }> = [];\n\n /** The smart objects whose contents are open (outermost first); empty when the document itself is shown. */\n public getEditingContents(): Array<{ layerId: string; name: string }> {\n return this.contentsStack.map((e) => ({ layerId: e.layerId, name: e.name }));\n }\n\n /**\n * Layer › Smart Objects › Edit Contents: shows the smart object's contents as a document of their\n * own (a layer document, or raster contents as one pixel layer), with their own undo history.\n * Every editor operation and tool then works on the contents until `closeSmartObjectContents`.\n * Contents can nest. Returns the contents' size and layer ids.\n */\n public editSmartObjectContents(id: string): { layerId: string; depth: number; width: number; height: number; layerIds: string[] } {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is open; commit or cancel it first.');\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are a ${c.file?.type ?? 'file'} that could not be decoded here; run prepareSmartObjects() in a browser first.`);\n const saved: SavedDocument = {\n state: { ...this.state },\n undo: this.undoStack,\n redo: this.redoStack,\n initial: this.initialSnapshot,\n element: this.imageElement,\n samplePixels: this.samplePixels,\n samplePixelsOverride: this.samplePixelsOverride,\n psdResolution: this.psdResolution,\n };\n const layers: ImageLayer[] =\n c.kind === 'document'\n ? c.document!.layers\n : [{ type: 'pixel', id: newLayerId(), name: 'Layer 0', visible: true, opacity: 1, blend: 'normal', surface: c.raster!, offset: { x: 0, y: 0 } }];\n this.contentsStack.push({ layerId: layer.id, contentsId: c.id, name: layer.name || c.name, saved });\n const s = this.state;\n this.state = {\n ...s,\n sourceUrl: null,\n imageDimensions: { width: c.width, height: c.height },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM_VALUES },\n layers,\n activeLayerId: layers.length ? layers[layers.length - 1].id : null,\n activeTarget: 'pixels',\n selection: null,\n selectedAnnotationId: null,\n outputOpacity: 1,\n zoom: 1,\n pan: { x: 0, y: 0 },\n editingContents: this.getEditingContents(),\n };\n delete this.state.legacyOutputBlur;\n if (c.document?.globalLight) this.state.globalLight = c.document.globalLight;\n else delete this.state.globalLight;\n if (c.document?.patterns?.length) this.state.patterns = c.document.patterns;\n else delete this.state.patterns;\n this.imageElement = null;\n this.samplePixels = null;\n this.samplePixelsOverride = false;\n this.undoStack = [];\n this.redoStack = [];\n this.viewKey = [];\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n return { layerId: layer.id, depth: this.contentsStack.length, width: c.width, height: c.height, layerIds: flattenLayers(layers).map((l) => l.id) };\n }\n\n /**\n * Closes the innermost open contents: with `save` (default) the edited layers become the smart\n * object's contents, and it and every linked instance render again (one undo step in the\n * document); without, the edits are dropped. Returns the smart object and the layers that changed.\n */\n public closeSmartObjectContents(save = true): { layerId: string; changed: boolean; layerIds: string[] } {\n const top = this.contentsStack.pop();\n if (!top) throw new LayerOperationError('No smart object contents are open; open them with edit_smart_object_contents.');\n if (this.openTransform?.isOpen) this.openTransform.cancel();\n this.openTransform = null;\n const edited = this.state;\n const changed = save && this.undoStack.length > 0;\n let next: SmartContents | null = null;\n if (changed) {\n const layer = findLayer(top.saved.state.layers, top.layerId)?.layer as SmartObjectLayer | undefined;\n const old = layer?.contents;\n const { width, height } = edited.imageDimensions;\n const only = edited.layers.length === 1 ? edited.layers[0] : null;\n const plain =\n only?.type === 'pixel' && only.visible && only.opacity === 1 && only.blend === 'normal' && !only.mask && !only.effects && !only.filters?.length && !only.clipped && (only.fillOpacity ?? 1) === 1 && only.source !== 'document';\n if (old?.kind === 'raster' && plain) {\n const px = readRegion(only.surface, { x: -only.offset.x, y: -only.offset.y, width, height });\n next = rasterContents({ width, height, data: px }, old.name, old.id);\n } else {\n const doc = { layers: edited.layers, globalLight: edited.globalLight, patterns: edited.patterns };\n next = documentContents(doc, width, height, old?.kind === 'document' ? old.name : `${(old?.name ?? 'Contents').replace(/\\.\\w+$/, '')}.psb`, top.contentsId);\n }\n }\n const saved = top.saved;\n this.state = saved.state;\n this.undoStack = saved.undo;\n this.redoStack = saved.redo;\n this.initialSnapshot = saved.initial;\n this.imageElement = saved.element;\n this.samplePixels = saved.samplePixels;\n this.samplePixelsOverride = saved.samplePixelsOverride;\n this.psdResolution = saved.psdResolution;\n this.viewKey = [];\n if (this.contentsStack.length) this.state.editingContents = this.getEditingContents();\n else delete this.state.editingContents;\n if (!next) {\n this.notify();\n return { layerId: top.layerId, changed: false, layerIds: [] };\n }\n const res = replaceContentsOp(this.state.layers, top.contentsId, next, this.canvasSize);\n this.commitLayers(res.layers);\n return { layerId: top.layerId, changed: true, layerIds: res.ids };\n }\n\n /** The document's own layers (not the contents open for editing). */\n private rootLayers(): ImageLayer[] {\n return this.contentsStack[0]?.saved.state.layers ?? this.state.layers;\n }\n\n /** Drops every open contents edit (loading another document). */\n private abandonContents(): void {\n while (this.contentsStack.length) this.closeSmartObjectContents(false);\n }\n\n // --- Layer effects ---\n\n /** A layer's effects (an empty, enabled list when it has none). */\n public getLayerEffects(id: string): LayerEffects {\n return layerEffectsOf(requireLayer(this.state.layers, id).layer);\n }\n\n /**\n * Adds an effect of `kind` with Photoshop's defaults and `params` applied (one undo step), at\n * `index` in the layer's effect list (default the end). Returns the new effect and the effects.\n */\n public addLayerEffect(id: string, kind: LayerEffectKind, params: Record<string, unknown> = {}, index?: number): { effect: LayerEffect; effects: LayerEffects } {\n const res = addLayerEffectOp(this.state.layers, id, kind, params, index);\n this.commitLayers(res.layers);\n return { effect: res.effect, effects: this.getLayerEffects(id) };\n }\n\n /** Changes one effect (one undo step, or none with `commitToHistory = false` during a drag). Returns the effects. */\n public updateLayerEffect(id: string, effectId: string, changes: Record<string, unknown>, commitToHistory = true): LayerEffects {\n this.commitLayers(updateLayerEffectOp(this.state.layers, id, effectId, changes), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n /** Removes one effect (one undo step). Returns the remaining effects. */\n public removeLayerEffect(id: string, effectId: string): LayerEffects {\n this.commitLayers(removeLayerEffectOp(this.state.layers, id, effectId));\n return this.getLayerEffects(id);\n }\n\n /** Turns a layer's effects on or off as a whole and, when given, sets Scale Effects (one undo step). */\n public setLayerEffectsEnabled(id: string, enabled: boolean, scale?: number, commitToHistory = true): LayerEffects {\n this.commitLayers(setLayerEffectsEnabledOp(this.state.layers, id, enabled, scale), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n /** Replaces a layer's effects (one undo step; `null` removes them). Returns the effects. */\n public setLayerEffects(id: string, effects: LayerEffects | null, commitToHistory = true): LayerEffects {\n this.commitLayers(setLayerEffectsOp(this.state.layers, id, effects), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n private patternIds = new WeakMap<object, number>();\n\n /** Changes whenever the document's pattern list is replaced (the compositor then drops its caches). */\n private patternsVersion(): number {\n const list = this.state.patterns;\n if (!list) return 0;\n let id = this.patternIds.get(list);\n if (id === undefined) {\n id = this.nextSourceId++;\n this.patternIds.set(list, id);\n }\n return id;\n }\n\n /** The document's patterns (from an opened PSD), besides the built-in ones. */\n public getDocumentPatterns(): EffectPattern[] {\n return this.state.patterns ?? [];\n }\n\n /** The light effects with \"use global light\" follow. */\n public getGlobalLight(): GlobalLight {\n return { ...(this.state.globalLight ?? DEFAULT_GLOBAL_LIGHT) };\n }\n\n /** Sets the global light (one undo step). `angle` -360..360°, `altitude` 0..90°. Returns it. */\n public setGlobalLight(light: Partial<GlobalLight>, commitToHistory = true): GlobalLight {\n const next = { ...this.getGlobalLight(), ...light };\n if (!(Number.isFinite(next.angle) && next.angle >= -360 && next.angle <= 360)) throw new LayerOperationError(`angle must be from -360 to 360 degrees, got ${next.angle}.`);\n if (!(Number.isFinite(next.altitude) && next.altitude >= 0 && next.altitude <= 90)) throw new LayerOperationError(`altitude must be from 0 to 90 degrees, got ${next.altitude}.`);\n if (commitToHistory) this.recordHistory();\n if (next.angle === DEFAULT_GLOBAL_LIGHT.angle && next.altitude === DEFAULT_GLOBAL_LIGHT.altitude) delete this.state.globalLight;\n else this.state.globalLight = next;\n this.notify();\n return this.getGlobalLight();\n }\n\n /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */\n private async compositeToSurface(layers: ImageLayer[], rect: CropBox): Promise<PixelSurface> {\n const { width, height } = this.naturalSize;\n if (flattenLayers(layers).some((l) => l.type === 'annotation' && l.visible) && !this.renderer.canRasterize()) {\n throw new LayerOperationError('Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.');\n }\n await this.renderer.prepareLayerAssets(layers, width, height);\n const data = new LayerCompositor().composite(layers, rect, this.renderer.compositorSources(width, height));\n return surfaceFromPixels({ width: rect.width, height: rect.height, data }, 4, rect.x, rect.y);\n }\n\n /** Document bounds a layer can paint into (pixel content, annotations, or the canvas for the rest). */\n private paintBounds(layer: ImageLayer): CropBox {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n if (layer.type === 'pixel') return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };\n if (layer.type === 'annotation') {\n const b = getAnnotationBounds(layer.annotation);\n const pad = 4 + ('strokeWidth' in layer.annotation ? (layer.annotation as { strokeWidth: number }).strokeWidth : 0);\n return coveringRect({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });\n }\n if (layer.type === 'group') {\n let r: CropBox | null = null;\n for (const c of layer.children) r = unionRect(r, this.paintBounds(c));\n return r ?? canvas;\n }\n return canvas;\n }\n\n /**\n * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,\n * opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.\n * Returns the merged id.\n */\n public async mergeDown(id: string): Promise<string> {\n const loc = requireLayer(this.state.layers, id);\n const upper = loc.layer;\n const siblings = loc.parent ? loc.parent.children : this.state.layers;\n const lower = siblings[loc.index - 1];\n if (!lower) throw new LayerOperationError(`Layer \"${upper.name || id}\" is the bottom of its group; there is nothing below to merge into.`);\n if (lower.type !== 'pixel') throw new LayerOperationError(`Merge Down needs a pixel layer below; \"${lower.name || lower.id}\" is a ${lower.type} layer. Move the layer or merge into another one.`);\n if (!upper.visible) throw new LayerOperationError(`Layer \"${upper.name || id}\" is hidden; show it or delete it instead of merging.`);\n requireUnlocked(upper, 'all', 'merge it');\n requireUnlocked(lower, 'pixels', 'merge into it');\n let rect = unionRect(this.paintBounds(lower), upper.type === 'adjustment' ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };\n // Effects paint beyond the layers' pixels: merge as far as they reach.\n const reach = Math.max(effectsMargin(lower.effects), upper.type === 'adjustment' ? 0 : effectsMargin(upper.effects));\n if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };\n const base: PixelLayer = { ...lower, blend: 'normal', opacity: 1 };\n delete base.clipped;\n const surface = await this.compositeToSurface([base, upper], rect);\n const merged: PixelLayer = { ...lower, surface, offset: { x: 0, y: 0 } };\n delete merged.mask;\n delete merged.fillOpacity;\n delete merged.filters;\n delete merged.source;\n delete merged.effects;\n let next = mapLayer(this.state.layers, id, () => null);\n next = mapLayer(next, lower.id, () => merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n });\n return merged.id;\n }\n\n /**\n * Merge Visible: the visible top-level layers become one normal pixel layer (at the position of\n * the lowest of them); hidden layers stay. One undo step. Returns the merged id.\n */\n public async mergeVisible(): Promise<string> {\n const visible = this.state.layers.filter((l) => l.visible);\n if (!visible.length) throw new LayerOperationError('No layer is visible.');\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(visible, { x: 0, y: 0, width, height });\n const merged: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: 'Merged',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n const at = this.state.layers.indexOf(visible[0]);\n const next = this.state.layers.filter((l) => !l.visible);\n next.splice(this.state.layers.slice(0, at).filter((l) => !l.visible).length, 0, merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return merged.id;\n }\n\n /** Flatten Image: every visible layer composited into one Background layer; hidden layers are discarded. One undo step. */\n public async flattenImage(): Promise<string> {\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(this.state.layers, { x: 0, y: 0, width, height });\n const bg: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n this.commitLayers([bg], () => {\n this.state.activeLayerId = bg.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return bg.id;\n }\n\n private thumbCache = new WeakMap<object, Map<string, PixelBuffer>>();\n\n /**\n * A small RGBA preview of a pixel layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px, point-sampled; `null` for layers without pixels.\n */\n public getLayerThumbnail(id: string, options: { maxSide?: number; mask?: boolean } = {}): PixelBuffer | null {\n const layer = this.getLayer(id);\n if (!layer) return null;\n const { width, height } = this.naturalSize;\n const side = Math.max(4, Math.min(256, Math.round(options.maxSide ?? 48)));\n const k = Math.min(1, side / Math.max(width, height));\n const tw = Math.max(1, Math.round(width * k));\n const th = Math.max(1, Math.round(height * k));\n const surface = options.mask ? (layer.mask && !layer.mask.source ? layer.mask.surface : null) : layer.type === 'pixel' || layer.type === 'smart' ? this.surfaceOf(layer) : null;\n if (!surface) return null;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const key = `${tw}x${th}:${ox},${oy}`;\n let byKey = this.thumbCache.get(surface);\n const hit = byKey?.get(key);\n if (hit) return hit;\n const out = new Uint8ClampedArray(tw * th * 4);\n for (let y = 0; y < th; y++) {\n const dy = Math.min(height - 1, Math.floor(((y + 0.5) * height) / th)) - oy;\n for (let x = 0; x < tw; x++) {\n const dx = Math.min(width - 1, Math.floor(((x + 0.5) * width) / tw)) - ox;\n const v = readPixel(surface, dx, dy);\n const i = (y * tw + x) * 4;\n if (surface.channels === 1) {\n out[i] = out[i + 1] = out[i + 2] = v[0];\n out[i + 3] = 255;\n } else out.set(v, i);\n }\n }\n const thumb = { width: tw, height: th, data: out };\n if (!byKey) {\n byKey = new Map();\n this.thumbCache.set(surface, byKey);\n }\n byKey.set(key, thumb);\n return thumb;\n }\n\n // --- Painting ---\n\n /** Brush settings, colours and tool options of the paint tools (view state, no undo). */\n public setPaintOptions(\n options: Partial<Omit<PaintOptions, 'brush' | 'mixer' | 'mixerLoad'>> & { brush?: Partial<BrushSettings>; mixer?: Partial<MixerSettings> }\n ): PaintOptions {\n const cur = this.state.paint;\n const next: PaintOptions = {\n brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,\n foreground: options.foreground !== undefined ? toHex(parseColor(options.foreground)) : cur.foreground,\n background: options.background !== undefined ? toHex(parseColor(options.background)) : cur.background,\n mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,\n mixerLoad: cur.mixerLoad,\n };\n this.state.paint = next;\n this.notify();\n return next;\n }\n\n /**\n * Loads the Mixer Brush's reservoir full with `color` (default the foreground); the picked-up\n * colour is kept. View state, no undo. Returns the brush load.\n */\n public loadMixerBrush(color?: string | number[]): MixerLoad {\n const c = parseColor(color ?? this.state.paint.foreground);\n const load = loadMixer(this.state.paint.mixerLoad, [c[0] / 255, c[1] / 255, c[2] / 255]);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Cleans the Mixer Brush: empties the pickup well (the reservoir is kept). Returns the brush load. */\n public cleanMixerBrush(): MixerLoad {\n const load = cleanMixer(this.state.paint.mixerLoad);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Empties the Mixer Brush entirely (no reservoir, nothing picked up). Returns the brush load. */\n public emptyMixerBrush(): MixerLoad {\n this.state.paint = { ...this.state.paint, mixerLoad: EMPTY_MIXER_LOAD };\n this.notify();\n return EMPTY_MIXER_LOAD;\n }\n\n /** D: black foreground, white background. */\n public resetPaintColors(): void {\n this.setPaintOptions({ foreground: '#000000', background: '#ffffff' });\n }\n\n /** X: swaps the foreground and background colours. */\n public swapPaintColors(): void {\n const { foreground, background } = this.state.paint;\n this.setPaintOptions({ foreground: background, background: foreground });\n }\n\n /** Resolves and validates what a stroke, fill or clear paints into. */\n private paintTarget(layerId: string | undefined, channel: LayerTarget | undefined, action: string) {\n const id = layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`);\n const layer = requireLayer(this.state.layers, id).layer;\n const ch: LayerTarget = channel ?? (id === this.state.activeLayerId ? this.state.activeTarget : 'pixels');\n if (ch === 'mask') {\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);\n if (layer.mask.source) throw new LayerOperationError(`Layer \"${layer.name || id}\" has a depth mask, which is generated, not painted.`);\n requireUnlocked(layer, 'all', action);\n } else if (ch === 'filter-mask') {\n if (layer.type !== 'smart' || !layer.smartFilters?.mask) {\n throw new LayerOperationError(`Layer \"${layer.name || id}\" has no smart filter mask to ${action}; add one with set_smart_filter_mask, or target pixels or mask.`);\n }\n if (!isContentsDecoded(layer.contents)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not decoded here, so its filter mask cannot be painted; run prepareSmartObjects() first.`);\n requireUnlocked(layer, 'all', action);\n } else {\n if (layer.type === 'smart') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);\n requireUnlocked(layer, 'pixels', action);\n }\n if (!layer.visible) throw new LayerOperationError(`Layer \"${layer.name || id}\" is hidden; show it before you ${action}.`);\n const offset = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const surface = ch === 'mask' ? layer.mask!.surface : ch === 'filter-mask' ? (layer as SmartObjectLayer).smartFilters!.mask!.surface : this.surfaceOf(layer as PixelLayer);\n const lockTransparency = ch === 'pixels' && Boolean(layer.locks?.transparency);\n return { id, layer, channel: ch, offset, surface, lockTransparency };\n }\n\n /** The layer with its pixels (or mask) replaced by `surface`. */\n private withSurface(layer: ImageLayer, channel: LayerTarget, surface: TiledSurface): ImageLayer {\n if (channel === 'mask') return { ...layer, mask: { ...layer.mask!, surface } } as ImageLayer;\n if (channel === 'filter-mask') return withFilterMaskSurface(layer as SmartObjectLayer, surface, this.canvasSize);\n const next = { ...(layer as PixelLayer), surface };\n delete next.source;\n return next;\n }\n\n /**\n * Starts a brush or eraser stroke on a layer's pixels or mask (default: the active layer and\n * target, the paint options' brush and colours). Feed points with `push`, finish with `end()`\n * (one undo step for the whole stroke) or `cancel()`. While it runs, the stack shows the stroke\n * without recording history.\n */\n public beginStroke(init: StrokeInit = {}): PaintStrokeSession {\n const tool = init.tool ?? 'brush';\n if (tool !== 'brush' && tool !== 'eraser' && tool !== 'mixer') throw new LayerOperationError(`Unknown paint tool \"${String(tool)}\"; use brush, eraser or mixer.`);\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : 'paint');\n if (tool === 'mixer' && target.channel !== 'pixels') {\n throw new LayerOperationError('The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.');\n }\n const brush = normalizeBrush({ ...this.state.paint.brush, ...(init.brush ?? {}) });\n const color = parseColor(init.color ?? this.state.paint.foreground);\n const background = parseColor(init.background ?? this.state.paint.background);\n const toMaskGrey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode: tool === 'eraser' ? 'erase' : 'paint',\n color: target.channel !== 'pixels' ? toMaskGrey(color) : color,\n background: target.channel !== 'pixels' ? toMaskGrey(background) : background,\n opacity: brush.opacity,\n blend: target.channel !== 'pixels' ? 'normal' : brush.blend,\n lockTransparency: target.lockTransparency,\n selection: this.state.selection?.bounds ? this.state.selection : null,\n offset: target.offset,\n };\n if (tool === 'mixer') return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */\n private mixerStroke(init: StrokeInit, brush: BrushSettings): MixerStrokeInit {\n const settings = normalizeMixer({ ...this.state.paint.mixer, ...(init.mixer ?? {}) });\n let load = this.state.paint.mixerLoad;\n if (init.color !== undefined || settings.loadEachStroke) {\n const c = parseColor(init.color ?? this.state.paint.foreground);\n load = loadMixer(load, [c[0] / 255, c[1] / 255, c[2] / 255]);\n }\n return {\n create: (pixels) => new MixerStroke(brush, settings, load, pixels),\n end: (after) => {\n const next = settings.cleanEachStroke ? cleanMixer(after) : after;\n this.state.paint = { ...this.state.paint, mixerLoad: next };\n return next;\n },\n };\n }\n\n /** @internal Shows a stroke's live surface (no history). */\n public _strokeUpdate(layerId: string, channel: LayerTarget, surface: TiledSurface, rects: CropBox[]): void {\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n // A filter mask re-renders the smart object (and feathering spreads the change): redraw it all.\n this.notify(channel === 'filter-mask' ? undefined : rects);\n }\n\n /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */\n public _strokeCommit(before: HistoryEntryHandle, layerId: string, channel: LayerTarget, surface: TiledSurface | null): void {\n if (surface) this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n else this.state.layers = (before as unknown as HistoryEntry).layers;\n if (surface && !this.isApplyingHistory) {\n this.undoStack.push(before as unknown as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n this.notify();\n }\n\n /** @internal The pre-stroke snapshot. */\n public _captureForStroke(): HistoryEntryHandle {\n return this.captureHistoryEntry() as unknown as HistoryEntryHandle;\n }\n\n /**\n * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional\n * pressure, through the brush, eraser or Mixer Brush. One undo step. Returns the layer, channel, the changed\n * document bounds and the number of dabs.\n */\n public paintStroke(init: StrokeInit & { points: StrokePoint[] }): PaintStrokeResult {\n if (!Array.isArray(init.points) || !init.points.length) throw new LayerOperationError('points must hold at least one { x, y } point.');\n for (const p of init.points) {\n if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) throw new LayerOperationError('Every point needs finite x and y (document pixels).');\n }\n const session = this.beginStroke(init);\n session.push(init.points);\n return session.end();\n }\n\n /**\n * Edit › Fill: fills the selection (or, without one or with `useSelection: false`, the whole\n * canvas) on a layer's pixels or mask with a colour (default the foreground), at `opacity` with\n * `blend`. One undo step. Returns the changed bounds.\n */\n public fillPixels(options: FillOptions = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'paint');\n }\n\n /**\n * Edit › Clear: removes the selected pixels (alpha to 0; on a transparency-locked layer or a mask\n * it fills with the background colour). Without a selection (or with `useSelection: false`) the\n * whole canvas. One undo step. Returns the changed bounds.\n */\n public clearPixels(options: Omit<FillOptions, 'color' | 'blend'> = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'erase');\n }\n\n private fillOrClear(options: FillOptions, mode: 'paint' | 'erase'): PaintStrokeResult {\n const target = this.paintTarget(options.layerId, options.channel, mode === 'paint' ? 'fill' : 'clear');\n const { width, height } = this.naturalSize;\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const blend = (parseBlend(options.blend) as BlendMode | undefined) ?? 'normal';\n const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;\n const region = sel?.bounds ?? { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const ones = new Float32Array(region.width * region.height);\n if (sel) {\n for (let y = 0; y < region.height; y++) {\n for (let x = 0; x < region.width; x++) ones[y * region.width + x] = sel.data[(region.y + y) * sel.width + region.x + x] / 255;\n }\n } else ones.fill(1);\n coverage.accumulate(region, ones, 1);\n const color = parseColor(options.color ?? this.state.paint.foreground);\n const background = parseColor(this.state.paint.background);\n const grey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode,\n color: target.channel !== 'pixels' ? grey(color) : color,\n background: target.channel !== 'pixels' ? grey(background) : background,\n opacity,\n blend: target.channel !== 'pixels' ? 'normal' : blend,\n lockTransparency: target.lockTransparency,\n selection: null,\n offset: target.offset,\n };\n const surface = paintCoverageInto(target.surface, coverage, apply, [region]);\n const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));\n this.commitLayers(next);\n return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };\n }\n\n // --- Adjustments ---\n\n /** The pixel layer the base sliders edit: the active pixel layer, else the Background. */\n private developLayer(): PixelLayer | null {\n return developTarget(this.state.layers, this.state.activeLayerId);\n }\n\n /**\n * Sets base adjustment values as one undo step (unless `commitToHistory` is false). The base\n * sliders edit the `develop` filter of the active pixel layer (or the Background), `blur` its\n * blur filter (or `legacyOutputBlur` in migrated documents), `opacity` the output opacity, and\n * `layers` replaces the adjustment layers.\n */\n public setAdjustments(partial: Partial<ImageAdjustments>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const target = this.developLayer();\n let layers = this.state.layers;\n if (target && (DEVELOP_KEYS.some((k) => k in partial) || 'process' in partial)) {\n layers = mapLayer(layers, target.id, (l) => withDevelop(l as PixelLayer, partial));\n }\n if ('blur' in partial && typeof partial.blur === 'number') {\n const blur = Math.max(0, partial.blur);\n if (this.state.legacyOutputBlur !== undefined) this.state.legacyOutputBlur = blur;\n else if (target) layers = mapLayer(layers, target.id, (l) => withBlur(l as PixelLayer, blur));\n }\n if ('opacity' in partial && typeof partial.opacity === 'number') {\n this.state.outputOpacity = Math.min(1, Math.max(0, partial.opacity));\n }\n if ('layers' in partial) {\n layers = this.replaceAdjustmentNodes(layers, normalizeAdjustmentLayers(partial.layers));\n }\n this.state.layers = layers;\n this.notify();\n }\n\n /** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */\n public get isProcessOutdated(): boolean {\n this.syncViews();\n return normalizeProcessVersion(this.state.adjustments.process) < LATEST_PROCESS_VERSION;\n }\n\n /**\n * Update to Current Process: re-interprets the current adjustment values with the latest\n * rendering process (one undo step). Returns false when already current.\n */\n public upgradeProcess(): boolean {\n if (!this.isProcessOutdated) return false;\n this.setAdjustments({ process: LATEST_PROCESS_VERSION });\n return true;\n }\n\n /** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */\n public resetAdjustments(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) =>\n withBlur(withDevelop(l as PixelLayer, { ...DEFAULT_ADJUSTMENTS, process: LATEST_PROCESS_VERSION }), 0)\n );\n }\n delete this.state.legacyOutputBlur;\n this.state.outputOpacity = 1;\n this.notify();\n }\n\n // --- Adjustment layers ---\n\n /** The adjustment layers in stack order, bottom (first) to top (last). */\n public getAdjustmentLayers(): AdjustmentLayer[] {\n this.syncViews();\n return this.state.adjustments.layers ?? [];\n }\n\n public getAdjustmentLayer(id: string): AdjustmentLayer | undefined {\n return this.getAdjustmentLayers().find((l) => l.id === id);\n }\n\n private adjustmentNodes(layers: readonly ImageLayer[] = this.state.layers): AdjustmentLayerNode[] {\n return flattenLayers(layers).filter((l): l is AdjustmentLayerNode => l.type === 'adjustment');\n }\n\n /**\n * Where a new layer goes when no position is given: directly above the topmost layer that is\n * not an annotation, so it adjusts what adjustment layers always adjusted (annotations stay on top).\n */\n private defaultInsertIndex(layers: readonly ImageLayer[] = this.state.layers): number {\n let i = layers.length;\n while (i > 0 && layers[i - 1].type === 'annotation') i--;\n return i;\n }\n\n /** Inserts `node` by `aboveId` / `parentId` / adjustment index / default position. */\n private placeLayer(\n layers: ImageLayer[],\n node: ImageLayer,\n where: { aboveId?: string; parentId?: string; adjustmentIndex?: number }\n ): ImageLayer[] {\n if (where.aboveId) {\n const loc = findLayer(layers, where.aboveId);\n if (loc) return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n if (where.parentId) {\n const loc = findLayer(layers, where.parentId);\n if (loc && loc.layer.type === 'group') return insertLayer(layers, node, loc.layer.id);\n }\n if (where.adjustmentIndex !== undefined) {\n const nodes = this.adjustmentNodes(layers);\n const at = Math.max(0, Math.min(nodes.length, Math.round(where.adjustmentIndex)));\n if (at < nodes.length) {\n const loc = findLayer(layers, nodes[at].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index);\n }\n if (nodes.length) {\n const loc = findLayer(layers, nodes[nodes.length - 1].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n }\n return insertLayer(layers, node, null, this.defaultInsertIndex(layers));\n }\n\n /** `setAdjustments({ layers })`: updates nodes by id in place, removes missing ones, adds new ones. */\n private replaceAdjustmentNodes(layers: ImageLayer[], next: AdjustmentLayer[]): ImageLayer[] {\n const { width, height } = this.naturalSize;\n const byId = new Map(next.map((l) => [l.id, l]));\n let out = layers;\n for (const node of this.adjustmentNodes(layers)) {\n const update = byId.get(node.id);\n out = mapLayer(out, node.id, () => (update ? mergeAdjustmentNode(node, update, width, height) : null));\n }\n const existing = new Set(this.adjustmentNodes(out).map((n) => n.id));\n for (const l of next) {\n if (existing.has(l.id)) continue;\n out = this.placeLayer(out, adjustmentNodeFromLegacy(l, width, height), { adjustmentIndex: Infinity });\n }\n // Keep the given order among the adjustment layers.\n const order = next.map((l) => l.id);\n for (let to = 0; to < order.length; to++) out = this.moveAdjustmentNode(out, order[to], to);\n return out;\n }\n\n /**\n * Adds an adjustment layer and returns its id, as one undo step. With no position it goes\n * above the other adjustment layers (or, without any, above the topmost non-annotation layer,\n * so annotations stay on top); `index` counts adjustment layers (0 = below the others);\n * `aboveId` / `parentId` place it anywhere in the stack. `kind` alone gives neutral parameters.\n */\n public addAdjustmentLayer<K extends AdjustmentKind>(init: K | AdjustmentLayerInit<K>, index?: number): string {\n const spec: AdjustmentLayerInit<K> = typeof init === 'string' ? { kind: init } : init;\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer = createAdjustmentLayer(spec.kind, {\n id: spec.id && !ids.has(spec.id) ? spec.id : undefined,\n name: spec.name,\n params: spec.params,\n enabled: spec.enabled,\n opacity: spec.opacity,\n blend: spec.blend,\n mask: spec.mask ?? undefined,\n });\n while (ids.has(layer.id)) layer.id = newLayerId('adj');\n const { width, height } = this.naturalSize;\n this.recordHistory();\n const node = adjustmentNodeFromLegacy(layer, width, height);\n const positioned = Boolean(spec.aboveId || spec.parentId);\n this.state.layers = this.placeLayer(this.state.layers, node, {\n aboveId: spec.aboveId,\n parentId: spec.parentId,\n adjustmentIndex: index !== undefined ? index : positioned ? undefined : Infinity,\n });\n this.notify();\n return layer.id;\n }\n\n /**\n * Updates a layer (`params` merged into the current ones, then normalised). Pass\n * `commitToHistory = false` for the moves of a drag after recording its start once.\n * Returns false for an unknown id.\n */\n public updateAdjustmentLayer(id: string, update: AdjustmentLayerUpdate, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n const { width, height } = this.naturalSize;\n const node = loc.layer;\n const merged = normalizeAdjustmentLayer({\n id: node.id,\n kind: node.kind,\n enabled: update.enabled ?? node.visible,\n opacity: update.opacity ?? node.opacity,\n blend: update.blend ?? node.blend,\n name: update.name ?? node.name,\n params: { ...node.params, ...(update.params ?? {}) },\n });\n if (!merged) return false;\n if (commitToHistory) this.recordHistory();\n const next = {\n ...node,\n visible: merged.enabled,\n opacity: merged.opacity,\n blend: merged.blend,\n name: merged.name ?? '',\n params: merged.params,\n } as AdjustmentLayerNode;\n if (update.mask !== undefined) {\n const mask = update.mask ? rasterMaskFromLegacy(update.mask, width, height) : null;\n if (mask) next.mask = mask;\n else delete next.mask;\n }\n this.state.layers = mapLayer(this.state.layers, id, () => next);\n this.notify();\n return true;\n }\n\n /** Removes a layer as one undo step. Returns false for an unknown id. */\n public removeAdjustmentLayer(id: string): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, id, () => null);\n if (this.state.activeLayerId === id) this.state.activeLayerId = null;\n this.notify();\n return true;\n }\n\n private moveAdjustmentNode(layers: ImageLayer[], id: string, toIndex: number): ImageLayer[] {\n const nodes = this.adjustmentNodes(layers);\n const from = nodes.findIndex((n) => n.id === id);\n if (from === -1) return layers;\n const to = Math.max(0, Math.min(nodes.length - 1, Math.round(toIndex)));\n if (to === from) return layers;\n const node = nodes[from];\n const anchor = nodes[to];\n const without = mapLayer(layers, id, () => null);\n const loc = findLayer(without, anchor.id)!;\n return insertLayer(without, node, loc.parent?.id ?? null, to > from ? loc.index + 1 : loc.index);\n }\n\n /** Moves a layer to `toIndex` among the adjustment layers (0 = bottom) as one undo step. Returns false if nothing moved. */\n public moveAdjustmentLayer(id: string, toIndex: number): boolean {\n const next = this.moveAdjustmentNode(this.state.layers, id, toIndex);\n if (next === this.state.layers) return false;\n this.recordHistory();\n this.state.layers = next;\n this.notify();\n return true;\n }\n\n /** Shows or hides a layer (flips it without `enabled`) as one undo step. */\n public toggleAdjustmentLayer(id: string, enabled?: boolean): boolean {\n const layer = this.getAdjustmentLayer(id);\n if (!layer) return false;\n const next = enabled ?? !layer.enabled;\n if (next === layer.enabled) return false;\n return this.updateAdjustmentLayer(id, { enabled: next });\n }\n\n /** Removes every adjustment layer as one undo step. */\n public clearAdjustmentLayers(): void {\n const nodes = this.adjustmentNodes();\n if (!nodes.length) return;\n this.recordHistory();\n let layers = this.state.layers;\n for (const n of nodes) layers = mapLayer(layers, n.id, () => null);\n this.state.layers = layers;\n if (nodes.some((n) => n.id === this.state.activeLayerId)) this.state.activeLayerId = null;\n this.notify();\n }\n\n // --- Selection ---\n\n /** The active selection, or `null`. */\n public getSelection(): SelectionMask | null {\n return this.state.selection ?? null;\n }\n\n public get hasSelection(): boolean {\n return Boolean(this.state.selection?.bounds);\n }\n\n private get naturalSize(): { width: number; height: number } {\n return {\n width: Math.max(1, Math.round(this.state.imageDimensions.width)),\n height: Math.max(1, Math.round(this.state.imageDimensions.height)),\n };\n }\n\n private dropMismatchedSelection(): void {\n const sel = this.state.selection;\n if (!sel) return;\n const { width, height } = this.naturalSize;\n if (sel.width !== width || sel.height !== height) this.state.selection = null;\n }\n\n /**\n * Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a\n * different size (e.g. an AI segmentation at another resolution) is resampled to the image.\n * Returns false when nothing changed.\n */\n public setSelection(mask: SelectionMask | null, mode: SelectionMode = 'replace'): boolean {\n const { width, height } = this.naturalSize;\n let next: SelectionMask | null = null;\n if (mask) {\n const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);\n next = combineSelections(this.state.selection, sized, mode);\n } else if (mode !== 'replace') {\n return false;\n }\n const prev = normalizeSelection(this.state.selection ?? null);\n if (prev === next || (prev && next && sameSelection(prev, next))) return false;\n this.recordHistory();\n this.state.selection = next;\n this.notify();\n return true;\n }\n\n /** Select › All. */\n public selectAll(): boolean {\n const { width, height } = this.naturalSize;\n return this.setSelection(fullSelection(width, height));\n }\n\n /** Select › Deselect. */\n public deselect(): boolean {\n return this.setSelection(null);\n }\n\n /** Select › Inverse (with nothing selected, selects everything). */\n public invertSelection(): boolean {\n const { width, height } = this.naturalSize;\n const current = this.state.selection ?? createSelectionMask(width, height);\n return this.setSelection(invertSelectionMask(current));\n }\n\n private shapeCommand(mask: SelectionMask, options: SelectionCommandOptions): boolean {\n const shaped = options.feather && options.feather > 0 ? modifySelectionMask(mask, 'feather', options.feather) : mask;\n return this.setSelection(shaped, options.mode ?? 'replace');\n }\n\n /** Rectangular marquee in natural image pixels. */\n public selectRect(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(rectSelection(width, height, rect, options), options);\n }\n\n /** Elliptical marquee inscribed in `rect`. */\n public selectEllipse(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(ellipseSelection(width, height, rect, options), options);\n }\n\n /** Lasso / polygonal lasso. */\n public selectPolygon(points: ImagePoint[], options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(polygonSelection(width, height, points, options), options);\n }\n\n /**\n * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker\n * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)\n * these are also the Background layer's pixels.\n */\n public setSamplePixels(pixels: PixelBuffer | null): void {\n this.samplePixels = pixels;\n this.samplePixelsOverride = pixels !== null;\n }\n\n /**\n * The pixels the selection tools sample: the source image at natural size, or with\n * `sampleMerged` the composite of the layers (no crop, rotation or annotations). `null`\n * without pixel access (no DOM, tainted canvas).\n */\n public async getSamplePixels(options: SampleOptions = {}): Promise<PixelBuffer | null> {\n const { width, height } = this.naturalSize;\n let base = options.pixels ?? this.samplePixels;\n if (!base) {\n try {\n const canvas = await this.renderSource();\n const ctx = canvas.getContext('2d');\n const data = ctx?.getImageData?.(0, 0, width, height);\n if (data && data.data && data.data.length >= width * height * 4) {\n base = { width, height, data: data.data };\n this.samplePixels = base;\n this.samplePixelsOverride = false;\n }\n } catch {\n base = null;\n }\n }\n if (!base) return null;\n if (base.width !== width || base.height !== height) {\n if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;\n return null;\n }\n const legacy = legacyView(this.stack(), width, height, null).exact;\n if (options.sample === 'active-layer') {\n const layer = this.getActiveLayer();\n if (!layer || layer.type !== 'pixel') {\n throw new LayerOperationError('Sampling the active layer needs an active pixel layer; select one, or sample the composite.');\n }\n const surface = layer.source === 'document' && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);\n return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };\n }\n if (legacy && !options.sampleMerged) return base;\n this.syncViews();\n if (legacy && !options.pixels) {\n // The old shape: the source through the base adjustments and adjustment layers.\n const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data as ArrayLike<number>) };\n applyAdjustments(copy as unknown as ImageData, legacyView(this.stack(), width, height, null).adjustments);\n return copy;\n }\n const pixels = options.pixels;\n const sources: CompositorSources = {\n width,\n height,\n surfaceOf: (l) => (l.type === 'pixel' && l.source === 'document' && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l)),\n version: pixels ? 'pixels' : this.sourceVersion(),\n };\n // Without `sampleMerged`, the pixels as painted: adjustment layers and filters left out.\n const stack = withoutAnnotations(this.state.layers);\n const layers = options.sampleMerged ? stack : pixelsOnly(stack);\n const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);\n return { width, height, data };\n }\n\n private async requirePixels(options: SampleOptions): Promise<PixelBuffer> {\n const pixels = await this.getSamplePixels(options);\n if (!pixels) throw new Error('The image pixels are not available (no canvas access); pass `pixels`.');\n return pixels;\n }\n\n /**\n * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when\n * nothing is selected).\n */\n public async magicWand(\n point: ImagePoint,\n options: MagicWandOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(magicWand(pixels, point, options), options);\n return this.getSelection();\n }\n\n /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */\n public async selectColorRange(\n samples: ColorRangeSample[],\n options: ColorRangeOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(colorRange(pixels, samples, options), options);\n return this.getSelection();\n }\n\n /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */\n public modifySelection(op: SelectionModifyOp, radius: number): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n return this.setSelection(modifySelectionMask(sel, op, radius));\n }\n\n /**\n * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)\n * to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.\n */\n public async refineSelection(params: RefineEdgeParams, options: SampleOptions = {}): Promise<boolean> {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;\n return this.setSelection(refineSelectionMask(pixels, sel, params));\n }\n\n /**\n * Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or\n * RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.\n */\n public async loadSelectionFromImage(\n source: string | PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number; mode?: SelectionMode } = {}\n ): Promise<SelectionMask | null> {\n let pixels: PixelBuffer;\n if (typeof source === 'string') {\n const img = await this.renderer.imageCache.get(source);\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const canvas = this.renderer.createCanvas(w, h);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, 0, 0, w, h);\n const data = ctx?.getImageData?.(0, 0, w, h);\n if (!data) throw new Error('The mask image pixels are not available (no canvas access); pass pixels.');\n pixels = { width: w, height: h, data: data.data };\n } else {\n pixels = source;\n }\n this.setSelection(selectionFromPixels(pixels, options), options.mode ?? 'replace');\n return this.getSelection();\n }\n\n /** Quick Mask view (tints the unselected area). View state: no undo step. */\n public setQuickMask(on: boolean): void {\n if (Boolean(this.state.quickMask) === on) return;\n this.state.quickMask = on;\n this.notify();\n }\n\n // --- Layer masks ---\n\n private maskFromSelection(sel: SelectionMask, props: LayerMaskProps = {}): RasterMask {\n const { width, height } = this.naturalSize;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const mask: RasterMask = { surface: surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0), defaultValue: 0 };\n return withMaskProps(mask, props);\n }\n\n /**\n * Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo\n * step. Without a selection, returns false.\n */\n public applySelectionAsLayerMask(layerId: string, props: LayerMaskProps = {}): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, this.maskFromSelection(sel, props));\n }\n\n private setLayerMask(layerId: string, mask: RasterMask | null, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, layerId);\n if (!loc) return false;\n if (commitToHistory) this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n if (!mask && this.state.activeLayerId === layerId) this.state.activeTarget = 'pixels';\n this.notify();\n return true;\n }\n\n /** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */\n public setAdjustmentLayerMask(layerId: string, mask: SelectionMask | null, props: LayerMaskProps = {}): boolean {\n if (!this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, mask ? this.maskFromSelection(mask, props) : null);\n }\n\n /** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */\n public setAdjustmentLayerMaskProps(layerId: string, props: LayerMaskProps, commitToHistory = true): boolean {\n const layer = this.getLayer(layerId);\n if (!layer || layer.type !== 'adjustment' || !layer.mask) return false;\n return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);\n }\n\n /** Load Selection from a layer mask (its pixels; density and feather are not applied). */\n public selectionFromLayerMask(layerId: string, mode: SelectionMode = 'replace'): boolean {\n const layer = this.getLayer(layerId);\n if (!layer?.mask || layer.mask.source) return false;\n const { width, height } = this.naturalSize;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });\n const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));\n return this.setSelection(sel, mode);\n }\n\n // --- Histogram ---\n\n /**\n * Histogram of the last rendered preview (after adjustments and layers, before annotations),\n * or `null` before the first render or without canvas pixel access.\n */\n public getHistogram(): ImageHistogram | null {\n return this.renderer.previewHistogram;\n }\n\n /** Calls `listener` now and whenever a render produces a new preview histogram. */\n public subscribeHistogram(listener: (histogram: ImageHistogram | null) => void): () => void {\n this.histogramListeners.add(listener);\n listener(this.renderer.previewHistogram);\n return () => {\n this.histogramListeners.delete(listener);\n };\n }\n\n // --- Depth Masking (Gleamforge) ---\n\n /** Sets the depth mask (a parametric mask on the active pixel layer, or the Background) as one undo step. */\n public setDepthMask(partial: Partial<DepthMaskOptions>): void {\n this.recordHistory();\n this.syncViews();\n const target = this.developLayer();\n if (target) {\n const merged: DepthMaskOptions = { ...this.state.depthMask, ...partial };\n merged.depthRange = [...merged.depthRange] as [number, number];\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n if (isDefaultDepthMask(merged)) {\n if (next.mask?.source) delete next.mask;\n } else next.mask = depthRasterMask(merged);\n return next;\n });\n }\n this.notify();\n }\n\n public resetDepthMask(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target?.mask?.source) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n delete next.mask;\n return next;\n });\n }\n this.notify();\n }\n\n // --- Annotations & Layers ---\n\n private annotationLayer(id: string): AnnotationLayer | undefined {\n return flattenLayers(this.state.layers).find(\n (l): l is AnnotationLayer => l.type === 'annotation' && l.annotation.id === id\n );\n }\n\n /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */\n public addAnnotation(annotation: CreateAnnotationPayload, placement: AnnotationPlacement = {}): string {\n this.recordHistory();\n const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n const fullAnnotation = { ...annotation, id } as Annotation;\n const layer = annotationLayerFor(fullAnnotation);\n const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;\n this.state.layers = loc\n ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1)\n : insertLayer(this.state.layers, layer, null);\n this.state.selectedAnnotationId = id;\n this.notify();\n return id;\n }\n\n public updateAnnotation(id: string, partial: Partial<Annotation>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const layer = this.annotationLayer(id);\n if (layer) {\n const annotation = { ...layer.annotation, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));\n }\n this.notify();\n }\n\n public removeAnnotation(id: string): void {\n this.recordHistory();\n const layer = this.annotationLayer(id);\n if (layer) this.state.layers = mapLayer(this.state.layers, layer.id, () => null);\n if (this.state.selectedAnnotationId === id) {\n this.state.selectedAnnotationId = null;\n }\n this.notify();\n }\n\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.layers = layers;\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n public selectAnnotation(id: string | null): void {\n this.state.selectedAnnotationId = id;\n this.notify();\n }\n\n // --- Zoom & Pan ---\n\n public setZoom(zoom: number): void {\n this.state.zoom = Math.max(0.1, Math.min(10, zoom));\n this.notify();\n }\n\n public zoomIn(factor = 1.2): void {\n this.setZoom(this.state.zoom * factor);\n }\n\n public zoomOut(factor = 1.2): void {\n this.setZoom(this.state.zoom / factor);\n }\n\n public resetZoom(): void {\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.notify();\n }\n\n public setPan(pan: ImagePoint): void {\n this.state.pan = pan;\n this.notify();\n }\n\n public setComparing(comparing: boolean): void {\n if (this.state.isOriginalCompared === comparing) return;\n this.state.isOriginalCompared = comparing;\n this.notify();\n }\n\n public setFit(fit: ImageEditorFit): void {\n if (this.state.fit === fit) return;\n this.state.fit = fit;\n this.notify();\n }\n\n // --- Rendering & Export ---\n\n private blankSource: HTMLCanvasElement | null = null;\n\n /**\n * What the renderer draws from: the source image, or, for a document without one (an opened\n * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every\n * pixel then comes from the layers. `null` when there is nothing to draw.\n */\n private drawSource(): CanvasImageSource | string | null {\n const source = this.imageElement || this.state.sourceUrl;\n if (source) return source;\n if (!this.isSourceless()) return null;\n const { width, height } = this.naturalSize;\n if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {\n this.blankSource = this.renderer.createCanvas(width, height);\n }\n return this.blankSource;\n }\n\n public async render(targetCanvas: HTMLCanvasElement, options?: RenderOptions): Promise<void> {\n const source = this.drawSource();\n if (!source) return;\n this.syncViews();\n const dirty = this.pendingDirty;\n this.pendingDirty = [];\n try {\n await this.renderer.renderToCanvas(targetCanvas, source, this.state, {\n ...options,\n dirty: dirty === 'all' ? undefined : dirty,\n checkpointLayerId: this.state.activeLayerId,\n });\n } catch {\n // In non-DOM or test mock environments without image loading, safely ignore\n }\n }\n\n public async renderExport(options?: ExportOptions): Promise<HTMLCanvasElement> {\n const source = this.drawSource();\n if (!source) {\n return this.renderer.createCanvas(1, 1);\n }\n this.syncViews();\n if (options?.layerIds) {\n const keep = new Set(options.layerIds);\n const layers = flattenLayers(this.state.layers).filter((l) => keep.has(l.id));\n return this.renderer.renderExport(source, { ...this.state, layers }, options);\n }\n return this.renderer.renderExport(source, this.state, options);\n }\n\n /**\n * Renders the edited image (adjustments, layers and, by default, annotations) in the\n * source image's own orientation and frame, ignoring crop, rotation and flips, so it lines\n * up with natural-pixel coordinates (selections, `select_region`). `scale` ≤ 1 shrinks it.\n * Returns `null` when no image is loaded.\n */\n public async renderSourceView(options: { scale?: number; includeAnnotations?: boolean } = {}): Promise<HTMLCanvasElement | null> {\n const source = this.drawSource();\n if (!source) return null;\n this.syncViews();\n const { width, height } = this.state.imageDimensions;\n const k = Math.min(1, Math.max(0, options.scale ?? 1));\n return this.renderer.renderExport(\n source,\n { ...this.state, crop: null, transform: { rotate: 0, flipH: false, flipV: false } },\n {\n width: Math.max(1, Math.round(width * k)),\n height: Math.max(1, Math.round(height * k)),\n includeAnnotations: options.includeAnnotations,\n }\n );\n }\n\n public async toDataURL(options: ExportOptions = {}): Promise<string> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n return canvas.toDataURL(format, quality);\n }\n\n public async toBlob(options: ExportOptions = {}): Promise<Blob> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Failed to create Blob from canvas'));\n }, format, quality);\n } else {\n // Fallback for environments lacking canvas.toBlob\n try {\n const dataUrl = canvas.toDataURL(format, quality);\n const parts = dataUrl.split(',');\n const byteStr = atob(parts[1]);\n const ab = new ArrayBuffer(byteStr.length);\n const ia = new Uint8Array(ab);\n for (let i = 0; i < byteStr.length; i++) {\n ia[i] = byteStr.charCodeAt(i);\n }\n resolve(new Blob([ab], { type: format }));\n } catch (err) {\n reject(err);\n }\n }\n });\n }\n\n // --- AI Masking & Transformation Helpers ---\n\n public async renderMask(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<HTMLCanvasElement> {\n this.syncViews();\n return this.renderer.renderMask(this.state, options);\n }\n\n public async toMaskDataURL(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<string> {\n const canvas = await this.renderMask(options);\n return canvas.toDataURL('image/png');\n }\n\n /**\n * Renders the current source image at natural size with no crop, rotation, flip,\n * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with\n * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.\n */\n public async renderSource(): Promise<HTMLCanvasElement> {\n const { width, height } = this.state.imageDimensions;\n const canvas = this.renderer.createCanvas(width, height);\n const source = this.imageElement || this.state.sourceUrl;\n const ctx = canvas.getContext('2d');\n if (!ctx || !source) return canvas;\n const img = typeof source === 'string' ? await this.renderer.imageCache.get(source) : source;\n ctx.drawImage(img, 0, 0, width, height);\n return canvas;\n }\n\n /**\n * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the\n * Background, or the composite of the layers without annotations, in the source frame, when\n * they go to a new layer or mask (see `applyInpaintedImage`).\n */\n public async toAIInputDataURL(options: AIResultOptions = {}): Promise<string> {\n const target = options.target ?? this.defaultAITarget();\n if (target === 'replace') return this.toSourceDataURL();\n const canvas = await this.renderSourceView({ includeAnnotations: false });\n return canvas ? canvas.toDataURL('image/png') : this.toSourceDataURL();\n }\n\n public async toSourceDataURL(options: Pick<ExportOptions, 'format' | 'quality'> = {}): Promise<string> {\n const canvas = await this.renderSource();\n return canvas.toDataURL(options.format || 'image/png', options.quality ?? 0.92);\n }\n\n /**\n * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment\n * and annotation layers are edits of the image, not images of their own).\n */\n /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */\n private defaultAITarget(): 'new-layer' | 'replace' {\n return this.isMultiLayer || this.isSourceless() ? 'new-layer' : 'replace';\n }\n\n /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */\n private isSourceless(): boolean {\n if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;\n return !flattenLayers(this.state.layers).some((l) => l.type === 'pixel' && l.source === 'document');\n }\n\n public get isMultiLayer(): boolean {\n return flattenLayers(this.state.layers).filter((l) => l.type === 'pixel' || l.type === 'group').length > 1;\n }\n\n /**\n * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.\n * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the\n * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise\n * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears\n * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.\n */\n public async applyInpaintedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, true);\n return;\n }\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const data = new Uint8ClampedArray(px.data as ArrayLike<number>);\n const sel = this.state.selection?.bounds ? this.state.selection : null;\n if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round((data[i] * sel.data[p]) / 255);\n return this.addPixelLayer({ name: 'Inpaint', pixels: { width, height, data } });\n }\n\n /**\n * Applies a background-removed result as ONE undoable step. In a document with several layers\n * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer\n * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer\n * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the\n * new image has different dimensions). Resolves to the masked layer's id in mask mode.\n */\n public async applyBackgroundRemovedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, false);\n return;\n }\n const target = this.developLayer();\n if (!target) throw new LayerOperationError('There is no pixel layer to mask.');\n requireUnlocked(target, 'all', 'mask it');\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const alpha = new Uint8ClampedArray(width * height);\n for (let p = 0; p < alpha.length; p++) alpha[p] = (px.data as ArrayLike<number>)[p * 4 + 3];\n let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);\n if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);\n this.commitLayers(setLayerMaskOp(this.state.layers, target.id, { surface, defaultValue: 0 }));\n return target.id;\n }\n\n /**\n * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and\n * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo\n * step. Returns the layer id.\n */\n public async rasterizeLayer(id: string): Promise<string> {\n const layer = requireLayer(this.state.layers, id).layer;\n requireUnlocked(layer, 'all', 'rasterize it');\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n let next: PixelLayer;\n if (layer.type === 'annotation') {\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const surface = await this.compositeToSurface([content], this.paintBounds(layer));\n next = { type: 'pixel', id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };\n if (layer.clipped) next.clipped = true;\n if (layer.mask) next.mask = layer.mask;\n if (layer.locks) next.locks = layer.locks;\n if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;\n } else if (layer.type === 'pixel') {\n if (!layer.filters?.length) throw new LayerOperationError(`Layer \"${layer.name || id}\" is already pixels with no filters to bake.`);\n requireUnlocked(layer, 'pixels', 'rasterize it');\n const spatial = layer.filters.some((f) => f.enabled && f.kind === 'blur');\n const content: PixelLayer = { ...layer, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;\n const surface = await this.compositeToSurface([content], rect);\n next = { ...layer, surface, offset: { x: 0, y: 0 } };\n delete next.filters;\n delete next.source;\n if (layer.mask && (layer.offset.x || layer.offset.y)) {\n // The mask lives in layer space: keep it where it was in the document.\n next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };\n }\n } else if (layer.type === 'smart') {\n return this.rasterizeSmartObject(id);\n } else {\n throw new LayerOperationError(`Only annotation layers, smart objects and pixel layers with filters rasterize; \"${layer.name || id}\" is a ${layer.type} layer. Merge it down or flatten instead.`);\n }\n this.commitLayers(mapLayer(this.state.layers, id, () => next));\n return id;\n }\n\n // --- PSD ---\n\n /** Pixels per inch of the last opened PSD (written back by `toPsd`). */\n private psdResolution: number | undefined;\n\n /**\n * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the\n * document has no source image afterwards (every pixel comes from the layers). What the model\n * cannot hold is approximated or left out; `warnings` says what, naming the layers.\n * Throws `PsdError` for files that cannot be read at all.\n */\n public loadPsd(bytes: Uint8Array): { layerIds: string[]; warnings: string[]; width: number; height: number; summary: PsdImportResult['summary'] } {\n const result = importPsd(bytes, { newId: () => newLayerId() });\n this.abandonContents();\n this.recordHistory();\n const s = this.state;\n this.imageElement = null;\n s.sourceUrl = null;\n s.imageDimensions = { width: result.width, height: result.height };\n if (!this.samplePixelsOverride) this.samplePixels = null;\n s.layers = result.layers;\n s.crop = null;\n s.transform = { ...DEFAULT_TRANSFORM_VALUES };\n s.outputOpacity = 1;\n delete s.legacyOutputBlur;\n s.selection = null;\n s.selectedAnnotationId = null;\n const pixels = flattenLayers(result.layers).filter((l) => l.type === 'pixel');\n s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;\n s.activeTarget = 'pixels';\n this.psdResolution = result.resolution;\n if (result.patterns.length) s.patterns = result.patterns;\n else delete s.patterns;\n if (result.globalLight) s.globalLight = result.globalLight;\n else delete s.globalLight;\n this.lastLoadProblems = result.warnings;\n this.notify();\n return {\n layerIds: flattenLayers(result.layers).map((l) => l.id),\n warnings: result.warnings,\n width: result.width,\n height: result.height,\n summary: result.summary,\n };\n }\n\n /**\n * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked\n * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left\n * out), and the merged image is our composite. `warnings` lists every such change.\n */\n public async toPsd(): Promise<{ bytes: Uint8Array; warnings: string[]; psb: boolean; width: number; height: number }> {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n const warnings: string[] = [];\n const note = new Map<string, string[]>();\n const add = (msg: string, name: string) => note.set(msg, [...(note.get(msg) ?? []), name]);\n const canRasterize = this.renderer.canRasterize();\n await this.renderer.prepareLayerAssets(this.state.layers, width, height);\n const sources = this.renderer.compositorSources(width, height);\n const compositor = new LayerCompositor();\n\n const bakeMask = (layer: ImageLayer): RasterMask | undefined => {\n const mask = layer.mask;\n if (!mask?.source) return mask;\n add('Parametric (depth) masks were baked into pixel masks', layer.name);\n const plain = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } } as ImageLayer;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const coverage = compositor.maskCoverage(plain, canvas, sources);\n const bytes = new Uint8ClampedArray(width * height);\n if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);\n else bytes.fill(255);\n const baked: RasterMask = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };\n if (mask.density !== undefined && mask.density < 1) baked.density = mask.density;\n if (mask.feather) baked.feather = mask.feather;\n if (mask.enabled === false) baked.enabled = false;\n return baked;\n };\n\n const prepare = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n const p = await prepareLayer(layer);\n // `exportPsd` writes it as Normal; the merged image keeps the paint mix.\n if (p?.blend === 'pigment') add('The Pigment blend mode has no PSD equivalent and was written as Normal', layer.name);\n return p;\n };\n\n const prepareLayer = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n if (layer.type === 'group') {\n const children: ImageLayer[] = [];\n for (const c of layer.children) {\n const p = await prepare(c);\n if (p) children.push(p);\n }\n return { ...layer, children, mask: bakeMask(layer) } as ImageLayer;\n }\n if (layer.type === 'adjustment') return { ...layer, mask: bakeMask(layer) } as ImageLayer;\n if (layer.type === 'annotation') {\n if (!canRasterize) {\n add('Annotation layers need a canvas (a browser) to rasterize and were left out', layer.name);\n return null;\n }\n add('Annotation layers were written as pixel layers', layer.name);\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend,\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.locks) out.locks = layer.locks;\n if (layer.effects) out.effects = layer.effects;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n return out;\n }\n if (layer.type === 'smart') {\n add('Smart objects were written as pixel layers (their rendered pixels)', layer.name);\n const px = rasterizeSmartObjectOp([layer], layer.id).layer;\n const mask = bakeMask(px);\n if (mask) px.mask = mask;\n return px;\n }\n let out: PixelLayer = { ...layer };\n if (layer.source === 'document') {\n out.surface = this.surfaceOf(layer);\n delete out.source;\n }\n const active = layer.filters?.filter(isActiveFilter) ?? [];\n if (active.length) {\n add('Layer filters (develop, blur) were baked into the pixels', layer.name);\n const content: PixelLayer = { ...out, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };\n }\n if (layer.filters?.some((f) => !f.enabled)) add('Disabled layer filters were left out', layer.name);\n delete out.filters;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n };\n\n const prepared: ImageLayer[] = [];\n for (const l of this.state.layers) {\n const p = await prepare(l);\n if (p) prepared.push(p);\n }\n const s = this.state;\n if (s.crop) warnings.push('The crop is not applied; the PSD is the uncropped document.');\n if (s.transform.rotate || s.transform.flipH || s.transform.flipV) warnings.push('Rotation and flips are not applied; the PSD is in the source orientation.');\n if (s.outputOpacity < 1) warnings.push('The document opacity is not written; layers keep their own opacity.');\n if (s.legacyOutputBlur) warnings.push('The document-level blur of an old document is not written.');\n for (const [msg, names] of note) warnings.push(`${msg} (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`);\n\n const merged = compositor.composite(prepared, canvas, { ...sources, version: `psd:${Date.now()}` });\n const result = exportPsd({ width, height, layers: prepared, merged, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight });\n warnings.push(...result.warnings);\n return { bytes: result.bytes, warnings, psb: result.psb, width, height };\n }\n\n private async replaceSource(src: string, clearAnnotations: boolean): Promise<void> {\n const next = await this.resolveSource(src);\n this.recordHistory();\n\n const prev = this.state.imageDimensions;\n const sx = next.dimensions.width / (prev.width || next.dimensions.width);\n const sy = next.dimensions.height / (prev.height || next.dimensions.height);\n\n this.imageElement = next.element;\n this.state.sourceUrl = next.sourceUrl;\n this.state.imageDimensions = next.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (this.state.selection && (sx !== 1 || sy !== 1)) {\n this.state.selection = normalizeSelection(\n resizeSelection(this.state.selection, Math.round(next.dimensions.width), Math.round(next.dimensions.height))\n );\n }\n\n let layers = this.state.layers;\n if (sx !== 1 || sy !== 1) {\n if (this.state.crop) {\n this.state.crop = clampCropToImage(\n {\n x: Math.round(this.state.crop.x * sx),\n y: Math.round(this.state.crop.y * sy),\n width: Math.round(this.state.crop.width * sx),\n height: Math.round(this.state.crop.height * sy),\n },\n next.dimensions.width,\n next.dimensions.height\n );\n }\n layers = resizeLegacyMasks(layers, prev, next.dimensions);\n for (const l of flattenLayers(layers)) {\n if (l.type === 'annotation') {\n layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));\n }\n }\n }\n // The Background now shows the new source.\n const bg = developTarget(layers, null);\n if (bg && bg.source !== 'document') layers = mapLayer(layers, bg.id, () => ({ ...bg, source: 'document' }));\n\n if (clearAnnotations) {\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.selectedAnnotationId = null;\n }\n this.state.layers = layers;\n this.notify();\n }\n\n // --- Serialization ---\n\n /**\n * The serializable state. A document whose layers still have the v1 / v2 shape (the\n * Background with its sliders and depth mask, adjustment layers, annotations) is written as\n * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG\n * assets. `selection` is included (run-length encoded) only when something is selected.\n */\n public toJSON(): SerializedImageEditorState;\n /**\n * With `assets: 'external'`, layer pixels go to `store` (each PNG uploaded once per store) and\n * the v3 document holds the store's references. Old-shape documents have no assets either way.\n */\n public toJSON(options: { assets: 'external'; store: ImageEditorAssetStore }): Promise<SerializedImageEditorState>;\n public toJSON(options?: { assets: 'external'; store: ImageEditorAssetStore }): SerializedImageEditorState | Promise<SerializedImageEditorState> {\n // While contents are open, the document is the one set aside (without the open edits).\n const root = this.contentsStack[0]?.saved;\n const current = this.state;\n const element = this.imageElement;\n if (root) {\n this.state = root.state;\n this.imageElement = root.element;\n this.viewKey = [];\n }\n let out: SerializedImageEditorState;\n try {\n out = this.serializeState();\n if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);\n } finally {\n if (root) {\n this.state = current;\n this.imageElement = element;\n this.viewKey = [];\n }\n }\n if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;\n return serializeLayersExternal(this.rootLayers(), options.store).then(({ layers, assets, smartContents }) => (smartContents ? { ...out, layers, assets, smartContents } : { ...out, layers, assets }));\n }\n\n /**\n * Loads a stored state whose v3 assets may live in an asset store (references other than data\n * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left\n * out and listed in `lastLoadProblems`.\n */\n public async loadJSONAsync(data: SerializedImageEditorState, options: { store?: ImageEditorAssetStore } = {}): Promise<void> {\n if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {\n this.loadJSON(data);\n return;\n }\n const problems: string[] = [];\n const assets = await resolveExternalAssets(data.assets, options.store, problems);\n this.loadJSON({ ...data, assets });\n this.lastLoadProblems = [...problems, ...this.lastLoadProblems];\n }\n\n /** Loads a stored state of any version (one undo step). */\n public loadJSON(data: SerializedImageEditorState): void {\n this.abandonContents();\n this.recordHistory();\n this.applyStored(data);\n let selection: SelectionMask | null = null;\n if (data.selection) {\n try {\n selection = normalizeSelection(deserializeSelection(data.selection));\n } catch {\n selection = null;\n }\n }\n this.state.selection = selection;\n this.dropMismatchedSelection();\n this.notify();\n }\n\n /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */\n public lastLoadProblems: string[] = [];\n\n private applyStored(raw: Partial<SerializedImageEditorState>): void {\n const s = this.state;\n const data = migrateSerializedImageEditorState(raw as SerializedImageEditorState);\n this.lastLoadProblems = [];\n if (isLayered(data)) {\n const problems: string[] = [];\n const canvas = typeof data.width === 'number' && data.width > 0 && typeof data.height === 'number' && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;\n s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });\n this.lastLoadProblems = problems;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.outputOpacity = data.opacity;\n if (typeof data.legacyOutputBlur === 'number' && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;\n const light = data.globalLight;\n if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };\n else delete s.globalLight;\n const patterns = deserializePatterns(data.patterns, problems);\n if (patterns.length) s.patterns = patterns;\n else delete s.patterns;\n if (!this.imageElement && typeof data.width === 'number' && typeof data.height === 'number' && data.width > 0 && data.height > 0) {\n s.imageDimensions = { width: data.width, height: data.height };\n }\n } else {\n const { width, height } = this.naturalSize;\n const stack = layersFromLegacy(data, width, height);\n s.layers = stack.layers;\n s.outputOpacity = stack.outputOpacity;\n if (stack.legacyOutputBlur !== undefined) s.legacyOutputBlur = stack.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.activeLayerId = null;\n delete s.globalLight;\n delete s.patterns;\n }\n s.activeTarget = 'pixels';\n this.viewKey = [];\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) s.selectedAnnotationId = null;\n }\n\n private serializeState(): SerializedImageEditorState {\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, null);\n // A global light is only stored in the layered schema.\n if (view.exact && !this.state.globalLight && !this.state.patterns?.length) {\n return {\n // States without adjustment layers keep schema 1, so they round-trip unchanged.\n version: view.adjustments.layers?.length ? 2 : 1,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n adjustments: serializeAdjustments(view.adjustments),\n depthMask: {\n ...view.depthMask,\n depthRange: [...view.depthMask.depthRange] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(view.annotations)),\n };\n }\n const { layers, assets, smartContents } = serializeLayers(this.state.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width,\n height,\n layers,\n assets,\n activeLayerId: this.state.activeLayerId,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n opacity: this.state.outputOpacity,\n };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };\n if (this.state.patterns?.length) out.patterns = serializePatterns(this.state.patterns);\n if (smartContents) out.smartContents = smartContents;\n return out;\n }\n}\n\n/**\n * Legacy-process adjustments are saved without `process` (and without an unused zero `tint`), so\n * they serialize exactly as they did before process versions existed.\n */\nfunction serializeAdjustments(adjustments: ImageAdjustments): ImageAdjustments {\n const out: ImageAdjustments = { ...adjustments };\n if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));\n else delete out.layers;\n if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {\n delete out.process;\n if (!out.tint) delete out.tint;\n }\n return out;\n}\n\n/** A pixel layer with its develop filter updated from base-slider values (created if missing). */\nfunction withDevelop(layer: PixelLayer, partial: Partial<ImageAdjustments>): PixelLayer {\n const filters = [...(layer.filters ?? [])];\n let i = filters.findIndex((f) => f.kind === 'develop');\n if (i === -1) {\n filters.unshift({\n id: layer.source === 'document' ? DEVELOP_FILTER_ID : newLayerId('flt'),\n kind: 'develop',\n enabled: true,\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n process: LATEST_PROCESS_VERSION,\n });\n i = 0;\n }\n const next = { ...(filters[i] as DevelopFilter) };\n for (const k of DEVELOP_KEYS) {\n const v = partial[k];\n if (typeof v === 'number' && Number.isFinite(v)) next[k] = v;\n }\n if ('process' in partial) next.process = normalizeProcessVersion(partial.process);\n filters[i] = next;\n return { ...layer, filters };\n}\n\n/** A pixel layer with its blur filter set to `radius` (removed at 0). */\nfunction withBlur(layer: PixelLayer, radius: number): PixelLayer {\n const existing = layer.filters?.find((f) => f.kind === 'blur');\n const filters: LayerFilter[] = (layer.filters ?? []).filter((f) => f.kind !== 'blur');\n if (radius > 0) {\n filters.push({\n id: existing?.id ?? (layer.source === 'document' ? BLUR_FILTER_ID : newLayerId('flt')),\n kind: 'blur',\n enabled: true,\n radius,\n });\n }\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return next;\n}\n\n/** Updates a node from an old-shape layer (`setAdjustments({ layers })`), keeping a raster mask whose pixels did not change. */\nfunction mergeAdjustmentNode(node: AdjustmentLayerNode, update: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const next = {\n ...node,\n kind: update.kind,\n params: JSON.parse(JSON.stringify(update.params)),\n visible: update.enabled !== false,\n opacity: update.opacity,\n blend: update.blend,\n name: update.name ?? '',\n } as AdjustmentLayerNode;\n if (!update.mask) delete next.mask;\n else {\n const current = node.mask && !node.mask.source ? node.mask : null;\n const memo = current ? legacyMaskOf(current, width, height) : null;\n if (current && memo && memo.width === update.mask.width && memo.height === update.mask.height && memo.rle === update.mask.rle) {\n next.mask = withMaskProps(current, {\n density: update.mask.density ?? 1,\n feather: update.mask.feather ?? 0,\n enabled: update.mask.enabled !== false,\n });\n } else {\n const m = rasterMaskFromLegacy(update.mask, width, height);\n if (m) next.mask = m;\n else delete next.mask;\n }\n }\n return next;\n}\n\n/** Applies mask property changes; `undefined` fields keep their value, defaults are left out. */\nfunction withMaskProps(mask: RasterMask, props: LayerMaskProps): RasterMask {\n const next: RasterMask = { ...mask };\n if (props.density !== undefined) {\n const d = Math.min(1, Math.max(0, props.density));\n if (d !== 1) next.density = d;\n else delete next.density;\n }\n if (props.feather !== undefined) {\n const f = Math.min(1000, Math.max(0, props.feather));\n if (f) next.feather = f;\n else delete next.feather;\n }\n if (props.enabled !== undefined) {\n if (props.enabled) delete next.enabled;\n else next.enabled = false;\n }\n if (props.linked !== undefined) {\n if (props.linked) delete next.linked;\n else next.linked = false;\n }\n return next;\n}\n\n/** Rescales adjustment-layer masks (canvas-sized) when the canvas size changes. */\nfunction resizeLegacyMasks(layers: ImageLayer[], from: ImageDimensions, to: ImageDimensions): ImageLayer[] {\n const fw = Math.max(1, Math.round(from.width));\n const fh = Math.max(1, Math.round(from.height));\n const tw = Math.max(1, Math.round(to.width));\n const th = Math.max(1, Math.round(to.height));\n if (fw === tw && fh === th) return layers;\n let out = layers;\n for (const l of flattenLayers(layers)) {\n if (l.type !== 'adjustment' || !l.mask || l.mask.source) continue;\n const old = l.mask;\n const mask = rasterMaskFromLegacy(legacyMaskOf(old, fw, fh), tw, th);\n if (!mask) continue;\n out = mapLayer(out, l.id, (n) => ({ ...n, mask: { ...mask, density: old.density, feather: old.feather, enabled: old.enabled } }));\n }\n return out;\n}\n\n/** Whether every pixel of `surface` inside the canvas is opaque. */\nfunction isOpaqueInside(surface: PixelSurface, width: number, height: number): boolean {\n const row = new Uint8ClampedArray(width * 4);\n for (let y = 0; y < height; y++) {\n readRegion(surface, { x: 0, y, width, height: 1 }, row);\n for (let i = 3; i < row.length; i += 4) if (row[i] !== 255) return false;\n }\n return true;\n}\n\nfunction collectStateTiles(layers: readonly ImageLayer[], into: Set<Tile>): void {\n for (const l of flattenLayers(layers)) {\n if (l.type === 'pixel' && l.source !== 'document') collectTiles(l.surface, into);\n if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);\n if (l.type === 'smart') {\n collectTiles(l.surface, into);\n if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);\n if (l.contents.raster) collectTiles(l.contents.raster, into);\n if (l.contents.document) collectStateTiles(l.contents.document.layers, into);\n }\n }\n}\n\n/** The stack without adjustment layers and layer filters (the pixels as painted). */\nfunction pixelsOnly(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'adjustment') continue;\n if (l.type === 'group') out.push({ ...l, children: pixelsOnly(l.children) });\n else if (l.type === 'pixel' && l.filters?.length) {\n const p = { ...l };\n delete p.filters;\n out.push(p);\n } else out.push(l);\n }\n return out;\n}\n\n/** Bilinear resample of RGBA pixels to `width × height`. */\nexport function resizePixels(px: PixelBuffer, width: number, height: number): PixelBuffer {\n const src = px.data as ArrayLike<number>;\n const out = new Uint8ClampedArray(width * height * 4);\n const kx = px.width / width;\n const ky = px.height / height;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(px.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(px.width - 1, x0 + 1);\n const tx = fx - x0;\n for (let c = 0; c < 4; c++) {\n const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;\n const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;\n out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;\n }\n }\n }\n return { width, height, data: out };\n}\n\nfunction withoutAnnotations(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: withoutAnnotations(l.children) } : l);\n }\n return out;\n}\n\n/** A colour as RGBA bytes: `#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` (0..255). */\nexport function parseColor(value: string | number[]): [number, number, number, number] {\n if (Array.isArray(value)) {\n if (value.length < 3 || value.length > 4 || !value.every((v) => typeof v === 'number' && v >= 0 && v <= 255)) {\n throw new LayerOperationError('A colour array is [r, g, b] or [r, g, b, a] with values 0..255.');\n }\n return [Math.round(value[0]), Math.round(value[1]), Math.round(value[2]), Math.round(value[3] ?? 255)];\n }\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(String(value).trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${value}\"; use #rrggbb or #rrggbbaa.`);\n let hex = m[1];\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n const n = (i: number) => parseInt(hex.slice(i, i + 2), 16);\n return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];\n}\n\n/** `#rrggbb` of RGBA bytes (alpha dropped). */\nexport function toHex(c: readonly number[]): string {\n return '#' + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');\n}\n\n/** `null` for an empty selection, so \"nothing selected\" has one representation. */\nfunction normalizeSelection(mask: SelectionMask | null): SelectionMask | null {\n return mask?.bounds ? mask : null;\n}\n\nfunction sameSelection(a: SelectionMask, b: SelectionMask): boolean {\n if (a.width !== b.width || a.height !== b.height) return false;\n const x = a.data;\n const y = b.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n return true;\n}\n\n/** Scales an annotation's geometry from one image size to another. */\nfunction scaleAnnotation(ann: Annotation, sx: number, sy: number): Annotation {\n const k = Math.sqrt(Math.abs(sx * sy));\n const base = { ...ann, x: ann.x * sx, y: ann.y * sy } as Annotation;\n switch (base.type) {\n case 'pen':\n return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };\n case 'rect':\n return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };\n case 'image':\n return { ...base, width: base.width * sx, height: base.height * sy };\n case 'circle':\n return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };\n case 'arrow':\n case 'line':\n return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };\n case 'text':\n return { ...base, fontSize: base.fontSize * k };\n }\n}\n","import {\n loadDocument,\n type JsonObject,\n type LoadReport,\n type MigrationStep,\n} from '../persistence/migrate';\nimport { DEFAULT_DEPTH_MASK, DEFAULT_TRANSFORM, normalizeStoredAdjustments } from './controller';\nimport type {\n Annotation,\n CropBox,\n DepthMaskOptions,\n ImageAdjustments,\n ImageTransform,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { deserializeSelection } from './selection/mask';\nimport type { SerializedLayer, SerializedLayeredImageEditorState } from './layers/serialize';\n\n/**\n * The newest `version` that `ImageEditorController.toJSON()` writes (3: layers). Documents whose\n * layers still have the old shape are written as version 1 or 2.\n */\nexport const IMAGE_EDITOR_STATE_VERSION = 3;\n\n/**\n * Migration steps for the document-level schema (versions 1 and 2), keyed by the version they\n * start from. Empty: version 2 only adds `adjustments.layers`, which version 1 readers keep.\n */\nexport const imageEditorStateMigrations: Record<number, MigrationStep> = {};\n\nexport interface ImageEditorDocumentLoad {\n /** State to pass to `loadJSON` / `initialState`. */\n state: SerializedImageEditorState;\n report: LoadReport;\n /** Top-level keys this build does not know; spread them into the saved JSON to keep them. */\n unknown: JsonObject;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\nfunction expectObject(value: unknown, what: string): Record<string, unknown> {\n if (!isObject(value)) throw new Error(`${what} must be an object`);\n return value;\n}\n\nfunction checkFields(\n value: Record<string, unknown>,\n types: Record<string, (v: unknown) => boolean>,\n what: string,\n) {\n for (const [field, ok] of Object.entries(types)) {\n if (field in value && value[field] !== undefined && !ok(value[field])) {\n throw new Error(`${what}.${field} has an invalid value`);\n }\n }\n}\n\nconst isBoolean = (v: unknown) => typeof v === 'boolean';\n\nfunction parseCrop(value: unknown): CropBox | null {\n if (value === null) return null;\n const crop = expectObject(value, 'crop');\n for (const field of ['x', 'y', 'width', 'height'] as const) {\n if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);\n }\n return { ...(crop as unknown as CropBox) };\n}\n\nfunction parseTransform(value: unknown): ImageTransform {\n const transform = expectObject(value, 'transform');\n checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, 'transform');\n return { ...DEFAULT_TRANSFORM, ...(transform as Partial<ImageTransform>) };\n}\n\nconst ADJUSTMENT_NUMBERS = [\n 'brightness',\n 'contrast',\n 'saturation',\n 'exposure',\n 'temperature',\n 'tint',\n 'blur',\n 'opacity',\n] as const;\n\nfunction parseAdjustments(value: unknown): ImageAdjustments {\n const adjustments = expectObject(value, 'adjustments');\n checkFields(\n adjustments,\n {\n ...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),\n process: (v) => v === 1 || v === 2,\n },\n 'adjustments',\n );\n // Kept as stored: `loadJSON` fills defaults and keeps a missing `process` legacy.\n return { ...(adjustments as unknown as ImageAdjustments) };\n}\n\nfunction parseDepthMask(value: unknown): DepthMaskOptions {\n const mask = expectObject(value, 'depthMask');\n checkFields(\n mask,\n {\n enabled: isBoolean,\n maskSource: (v) => v === null || typeof v === 'string',\n mode: (v) => v === 'threshold' || v === 'brightness',\n depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),\n softness: isNumber,\n invert: isBoolean,\n },\n 'depthMask',\n );\n const merged = { ...DEFAULT_DEPTH_MASK, ...(mask as Partial<DepthMaskOptions>) };\n return { ...merged, depthRange: [...merged.depthRange] as [number, number] };\n}\n\nconst ANNOTATION_TYPES = new Set(['pen', 'rect', 'circle', 'arrow', 'line', 'text', 'image']);\n\nconst isAnnotation = (value: unknown): value is Annotation =>\n isObject(value) &&\n typeof value.id === 'string' &&\n typeof value.type === 'string' &&\n ANNOTATION_TYPES.has(value.type) &&\n isNumber(value.x) &&\n isNumber(value.y);\n\n/**\n * Loads stored image editor state (a JSON string or object) with a `LoadReport`:\n *\n * - Runs `imageEditorStateMigrations` up to `IMAGE_EDITOR_STATE_VERSION`; a missing\n * `version` is taken as current.\n * - A newer `version` is still loaded but reported `readOnly`: do not save over it.\n * - Each section (`crop`, `transform`, `adjustments`, `depthMask`, `annotations`, and the\n * optional run-length encoded `selection`)\n * falls back to its default on its own; invalid annotations are dropped one by one.\n * - Unknown top-level keys are returned in `unknown`.\n *\n * Valid state from `toJSON()` comes back unchanged, so `loadJSON(state)` behaves\n * exactly as with the raw object.\n */\nexport function loadImageEditorDocument(input: unknown): ImageEditorDocumentLoad {\n let parsed: unknown = input;\n if (typeof input === 'string') {\n try {\n parsed = JSON.parse(input);\n } catch {\n parsed = input;\n }\n }\n if (isObject(parsed) && parsed.version === 3) return loadLayeredDocument(parsed);\n // Version 2 only adds `adjustments.layers`, which the version 1 sections keep as stored.\n const isV2 = isObject(parsed) && parsed.version === 2;\n if (isV2) parsed = { ...(parsed as Record<string, unknown>), version: 1 };\n const newer = isObject(parsed) && typeof parsed.version === 'number' && parsed.version > IMAGE_EDITOR_STATE_VERSION;\n let dropped = 0;\n const result = loadDocument<Omit<LegacySerializedImageEditorState, 'version'>>(\n parsed,\n {\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n adjustments: {\n parse: parseAdjustments,\n // Same as what `loadJSON` derives from missing adjustments.\n defaults: () => normalizeStoredAdjustments(undefined),\n },\n depthMask: {\n parse: parseDepthMask,\n defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] as [number, number] }),\n },\n annotations: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('annotations must be an array');\n const kept = value.filter(isAnnotation);\n dropped = value.length - kept.length;\n return JSON.parse(JSON.stringify(kept)) as Annotation[];\n },\n defaults: () => [],\n },\n // Optional: only present when something was selected when saving.\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n {\n // Versions 1 and 2 load as one schema; anything newer than 3 is reported read-only.\n currentVersion: newer ? IMAGE_EDITOR_STATE_VERSION : 1,\n steps: imageEditorStateMigrations,\n },\n );\n if (dropped > 0) {\n result.report.problems.push({\n section: 'annotations',\n message: `${dropped} invalid annotation${dropped === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 1, ...result.value } as LegacySerializedImageEditorState;\n if (isV2) {\n if (state.adjustments.layers?.length) state.version = 2;\n result.report.version = 2;\n }\n if (state.selection === undefined) delete state.selection;\n return { state, report: result.report, unknown: result.unknown };\n}\n\nconst LAYER_TYPES = new Set(['pixel', 'adjustment', 'group', 'annotation', 'smart']);\n\n/** Keeps layers that have an id-like shape and a known type (children checked too); counts the rest. */\nfunction cleanLayers(value: unknown[], dropped: { count: number }): SerializedLayer[] {\n const out: SerializedLayer[] = [];\n for (const raw of value) {\n if (!isObject(raw) || typeof raw.type !== 'string' || !LAYER_TYPES.has(raw.type)) {\n dropped.count++;\n continue;\n }\n if (raw.type === 'group') {\n const children = Array.isArray(raw.children) ? cleanLayers(raw.children, dropped) : [];\n out.push({ ...(raw as unknown as SerializedLayer), children } as SerializedLayer);\n } else out.push(JSON.parse(JSON.stringify(raw)) as SerializedLayer);\n }\n return out;\n}\n\n/** Version 3 (layers): each section falls back on its own; unreadable layers are dropped one by one. */\nfunction loadLayeredDocument(doc: Record<string, unknown>): ImageEditorDocumentLoad {\n const dropped = { count: 0 };\n const result = loadDocument<Omit<SerializedLayeredImageEditorState, 'version'>>(\n doc,\n {\n width: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('width must be a positive number'); })()), defaults: 0 },\n height: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('height must be a positive number'); })()), defaults: 0 },\n layers: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('layers must be an array');\n return cleanLayers(value, dropped);\n },\n defaults: () => [],\n },\n assets: {\n parse: (value) => {\n const assets = expectObject(value, 'assets');\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(assets)) if (typeof v === 'string') out[k] = v;\n return out;\n },\n defaults: () => ({}),\n },\n activeLayerId: { parse: (v) => (v === null || typeof v === 'string' ? v : null), defaults: null },\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n opacity: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('opacity must be a number');\n return Math.min(1, Math.max(0, v));\n },\n defaults: 1,\n },\n legacyOutputBlur: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('legacyOutputBlur must be a number');\n return Math.max(0, v);\n },\n defaults: undefined,\n },\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n smartContents: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('smartContents must be an array');\n // Entries are checked one by one when the layers load; keep the ones with an id.\n return value.filter((c) => isObject(c) && typeof c.id === 'string').map((c) => JSON.parse(JSON.stringify(c)));\n },\n defaults: undefined,\n },\n },\n { currentVersion: 3, steps: {} },\n );\n if (dropped.count > 0) {\n result.report.problems.push({\n section: 'layers',\n message: `${dropped.count} invalid layer${dropped.count === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 3, ...result.value } as SerializedLayeredImageEditorState;\n if (state.selection === undefined) delete state.selection;\n if (state.legacyOutputBlur === undefined) delete state.legacyOutputBlur;\n if (state.smartContents === undefined) delete state.smartContents;\n return { state, report: result.report, unknown: result.unknown };\n}\n","// Portions ported from lightcraft-main/crates/develop/src/controls.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentKind, ImageAdjustments } from './types';\n\n/**\n * Control specs: one table describes every numeric adjustment slider (id, label, section, range,\n * default, step, display precision and track style). The React and Svelte panels render from it,\n * the agent tools expose it as a schema, and values are addressed by id.\n */\n\nexport type ControlSection = 'light' | 'color' | 'effects' | 'layer';\n\n/** How a slider track is drawn. */\nexport type ControlTrack =\n /** Plain track, fill from the minimum. */\n | { kind: 'plain' }\n /** Zero in the middle. */\n | { kind: 'centered' }\n /** Blue → yellow (white-balance temperature). */\n | { kind: 'temp' }\n /** Green → magenta (tint). */\n | { kind: 'tint' }\n /** A two-colour gradient. */\n | { kind: 'gradient'; from: string; to: string }\n /** Full hue rainbow. */\n | { kind: 'rainbow' };\n\nexport interface ControlSpec {\n /** Dotted path of the value (`exposure`; inside a layer's params: `midtones.0`, `ranges.4.hue`). */\n id: string;\n label: string;\n section: ControlSection;\n min: number;\n max: number;\n step: number;\n default: number;\n /** Decimal places shown (after `displayScale`). */\n decimals: number;\n track: ControlTrack;\n /** Multiplier for display (opacity 0..1 shows as 0..100 %). */\n displayScale?: number;\n /** Unit suffix for display (`px`, `%`, `°`). */\n unit?: string;\n /** The rendering process the control needs (tint: process 2). */\n requiresProcess?: 1 | 2;\n}\n\nconst plain: ControlTrack = { kind: 'plain' };\nconst centered: ControlTrack = { kind: 'centered' };\n\n/** Every numeric base adjustment, in panel order. */\nexport const IMAGE_ADJUSTMENT_CONTROLS: readonly ControlSpec[] = [\n { id: 'brightness', label: 'Brightness', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'contrast', label: 'Contrast', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n {\n id: 'saturation', label: 'Saturation', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' },\n },\n { id: 'exposure', label: 'Exposure', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'temperature', label: 'Warmth / Temperature', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'temp' } },\n { id: 'tint', label: 'Tint', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'tint' }, requiresProcess: 2 },\n { id: 'blur', label: 'Blur', section: 'effects', min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: 'px' },\n { id: 'opacity', label: 'Opacity', section: 'effects', min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\nconst tone = (prefix: string, label: string): ControlSpec[] =>\n [\n ['0', 'Cyan – Red', '#00b7c7', '#e0343a'],\n ['1', 'Magenta – Green', '#d23ad2', '#2fb34a'],\n ['2', 'Yellow – Blue', '#e0c63a', '#3a5be0'],\n ].map(([i, l, from, to]) => ({\n id: `${prefix}.${i}`, label: `${label} ${l}`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from, to },\n }));\n\nconst levelsRecord = (ch: string, label: string): ControlSpec[] => [\n { id: `${ch}.inBlack`, label: `${label} input black`, section: 'layer', min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.gamma`, label: `${label} gamma`, section: 'layer', min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },\n { id: `${ch}.inWhite`, label: `${label} input white`, section: 'layer', min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n { id: `${ch}.outBlack`, label: `${label} output black`, section: 'layer', min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.outWhite`, label: `${label} output white`, section: 'layer', min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n];\n\nconst RANGE_NAMES = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'];\n\n/**\n * Numeric parameters of each adjustment-layer kind, addressed inside `layer.params`. Curves points\n * and booleans / colours (colorize, preserve luminosity, tint, filter colour) are edited by the\n * dedicated controls, not sliders.\n */\nexport const ADJUSTMENT_LAYER_CONTROLS: Readonly<Record<AdjustmentKind, readonly ControlSpec[]>> = {\n curves: [],\n levels: [\n ...levelsRecord('master', 'RGB'),\n ...levelsRecord('red', 'Red'),\n ...levelsRecord('green', 'Green'),\n ...levelsRecord('blue', 'Blue'),\n ],\n hueSaturation: [\n { id: 'hue', label: 'Hue', section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n { id: 'lightness', label: 'Lightness', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } },\n ...RANGE_NAMES.flatMap((name, i): ControlSpec[] => [\n { id: `ranges.${i}.hue`, label: `${name} hue`, section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: `ranges.${i}.saturation`, label: `${name} saturation`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: `ranges.${i}.lightness`, label: `${name} lightness`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n ]),\n ],\n vibrance: [\n { id: 'vibrance', label: 'Vibrance', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#d8406a' } },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n ],\n colorBalance: [...tone('shadows', 'Shadows'), ...tone('midtones', 'Midtones'), ...tone('highlights', 'Highlights')],\n blackWhite: [\n ['reds', 'Reds', 40, '#e0343a'],\n ['yellows', 'Yellows', 60, '#e0c63a'],\n ['greens', 'Greens', 40, '#2fb34a'],\n ['cyans', 'Cyans', 60, '#00b7c7'],\n ['blues', 'Blues', 20, '#3a5be0'],\n ['magentas', 'Magentas', 80, '#d23ad2'],\n ].map(([id, label, def, to]) => ({\n id: id as string, label: label as string, section: 'layer' as const, min: -200, max: 300, step: 1, default: def as number, decimals: 0,\n track: { kind: 'gradient' as const, from: '#000000', to: to as string }, unit: '%',\n })),\n invert: [],\n posterize: [{ id: 'levels', label: 'Levels', section: 'layer', min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],\n threshold: [{ id: 'level', label: 'Threshold level', section: 'layer', min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } }],\n photoFilter: [{ id: 'density', label: 'Density', section: 'layer', min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: '%' }],\n};\n\n/** Opacity of any adjustment layer (0..1, shown as %). */\nexport const ADJUSTMENT_LAYER_OPACITY_CONTROL: ControlSpec = {\n id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%',\n};\n\nconst BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));\n\n/** The base-adjustment control with this id. */\nexport function findAdjustmentControl(id: string): ControlSpec | undefined {\n return BASE_BY_ID.get(id);\n}\n\n/** The control of a layer parameter (`opacity` included). */\nexport function findLayerControl(kind: AdjustmentKind, id: string): ControlSpec | undefined {\n if (id === 'opacity') return ADJUSTMENT_LAYER_OPACITY_CONTROL;\n return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);\n}\n\n/** `v` clamped to the control's range and snapped to its step; non-finite values give the default. */\nexport function clampControlValue(spec: ControlSpec, v: unknown): number {\n if (typeof v !== 'number' || !Number.isFinite(v)) return spec.default;\n const clamped = Math.min(spec.max, Math.max(spec.min, v));\n const steps = Math.round((clamped - spec.min) / spec.step);\n const snapped = spec.min + steps * spec.step;\n // Trim float noise from the step arithmetic.\n return Math.min(spec.max, Number(snapped.toFixed(6)));\n}\n\n/** A value as the slider shows it (`+12`, `35%`, `4px`). */\nexport function formatControlValue(spec: ControlSpec, v: number): string {\n const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);\n const s = shown.toFixed(spec.decimals);\n return `${s}${spec.unit ?? ''}`;\n}\n\n/** Where `v` sits on the track, 0 (left) to 1 (right). */\nexport function controlPosition(spec: ControlSpec, v: number): number {\n const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);\n return Math.min(1, Math.max(0, p));\n}\n\n/** CSS background for a control's track. */\nexport function controlTrackBackground(spec: ControlSpec): string | undefined {\n const t = spec.track;\n switch (t.kind) {\n case 'temp':\n return 'linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)';\n case 'tint':\n return 'linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)';\n case 'gradient':\n return `linear-gradient(to right, ${t.from}, ${t.to})`;\n case 'rainbow':\n return 'linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)';\n default:\n return undefined;\n }\n}\n\n/** Reads a dotted path (`ranges.2.hue`) from an object. */\nexport function getControlPath(obj: unknown, path: string): unknown {\n let cur: any = obj;\n for (const part of path.split('.')) {\n if (cur === null || cur === undefined) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\n/** A copy of `obj` with the dotted path set (arrays and objects along the path are cloned). */\nexport function setControlPath<T>(obj: T, path: string, value: unknown): T {\n const parts = path.split('.');\n const root: any = Array.isArray(obj) ? [...(obj as unknown[])] : { ...(obj as object) };\n let cur = root;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = cur[parts[i]];\n cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === 'object' ? { ...next } : {};\n cur = cur[parts[i]];\n }\n cur[parts[parts.length - 1]] = value;\n return root as T;\n}\n\n/** The base adjustment values the control table describes. */\nexport function readAdjustmentControls(adjustments: ImageAdjustments): Record<string, number> {\n const out: Record<string, number> = {};\n for (const c of IMAGE_ADJUSTMENT_CONTROLS) {\n const v = (adjustments as unknown as Record<string, unknown>)[c.id];\n out[c.id] = typeof v === 'number' && Number.isFinite(v) ? v : c.default;\n }\n return out;\n}\n","import type { SelectionMask, SelectionMode } from './mask';\nimport type { ColorDistance } from './wand';\nimport type { RefineEdgeParams } from './refine';\n\n/** Selection tools of the image editor UI. */\nexport type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'lasso';\n\nexport const SELECTION_TOOLS: readonly { id: SelectionToolKind; label: string }[] = [\n { id: 'wand', label: 'Magic Wand' },\n { id: 'rect', label: 'Rectangle' },\n { id: 'ellipse', label: 'Ellipse' },\n { id: 'lasso', label: 'Lasso' },\n];\n\nexport const SELECTION_MODE_LABELS: Record<SelectionMode, string> = {\n replace: 'New',\n add: 'Add',\n subtract: 'Subtract',\n intersect: 'Intersect',\n};\n\n/** The options bar of the selection tools (shared by the React and Svelte UIs). */\nexport interface SelectionToolOptions {\n tool: SelectionToolKind;\n mode: SelectionMode;\n /** Magic wand tolerance, 0..255. */\n tolerance: number;\n contiguous: boolean;\n antiAlias: boolean;\n /** Sample the adjusted image instead of the source. */\n sampleMerged: boolean;\n /** Sample the whole image (`composite`, default) or only the active pixel layer. */\n sample?: 'composite' | 'active-layer';\n distance: ColorDistance;\n}\n\nexport const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions = {\n tool: 'wand',\n mode: 'replace',\n tolerance: 32,\n contiguous: true,\n antiAlias: true,\n sampleMerged: false,\n distance: 'rgb',\n};\n\n/**\n * Photoshop's modifier keys: Shift adds, Alt/Option subtracts, both intersect; otherwise the\n * options bar mode.\n */\nexport function selectionModeFromModifiers(\n modifiers: { shiftKey?: boolean; altKey?: boolean },\n fallback: SelectionMode\n): SelectionMode {\n if (modifiers.shiftKey && modifiers.altKey) return 'intersect';\n if (modifiers.shiftKey) return 'add';\n if (modifiers.altKey) return 'subtract';\n return fallback;\n}\n\nexport interface RefineEdgeControl {\n id: keyof Omit<RefineEdgeParams, 'smartRadius'>;\n label: string;\n min: number;\n max: number;\n step: number;\n unit: string;\n}\n\n/** Sliders of the Refine Edge panel. */\nexport const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[] = [\n { id: 'radius', label: 'Radius', min: 0, max: 64, step: 1, unit: 'px' },\n { id: 'smooth', label: 'Smooth', min: 0, max: 100, step: 1, unit: '' },\n { id: 'feather', label: 'Feather', min: 0, max: 50, step: 0.5, unit: 'px' },\n { id: 'contrast', label: 'Contrast', min: 0, max: 100, step: 1, unit: '%' },\n { id: 'shiftEdge', label: 'Shift Edge', min: -100, max: 100, step: 1, unit: '%' },\n];\n\n/** A one-line status: \"No selection\" or \"W × H px selected (n % of the image)\". */\nexport function describeSelection(mask: SelectionMask | null | undefined): string {\n if (!mask || !mask.bounds) return 'No selection';\n let sum = 0;\n for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];\n const pct = (sum / 255 / mask.data.length) * 100;\n const shown = pct >= 10 ? Math.round(pct) : Math.round(pct * 10) / 10;\n return `${mask.bounds.width} × ${mask.bounds.height} px selected (${shown}% of the image)`;\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind } from './types';\nimport { ADJUSTMENT_KINDS, isAdjustmentKind } from './adjustment-layers';\nimport { describeSelection } from './selection/tool-options';\nimport { SELECTION_MODES, parseSelectionMode, selectionArea } from './selection/mask';\nimport { SELECTION_MODIFY_OPS, type SelectionModifyOp } from './selection/modify';\nimport { layerMaskFromSelection } from './selection/layer-mask';\n\nconst modeSchema: JsonSchema = {\n type: 'string',\n enum: [...SELECTION_MODES],\n description: 'How the result combines with the current selection (default replace).',\n};\n\nconst num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\nfunction summary(controller: ImageEditorController) {\n const sel = controller.getSelection();\n const { width, height } = controller.getState().imageDimensions;\n const area = selectionArea(sel);\n return {\n empty: !sel,\n bounds: sel?.bounds ?? null,\n /** Σ coverage in pixels. */\n area: Math.round(area),\n percent: Math.round((area / Math.max(1, width * height)) * 10000) / 100,\n description: describeSelection(sel),\n };\n}\n\nconst writeAnnotations = (title: string, idempotent = false) => ({\n title,\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: idempotent,\n openWorldHint: false,\n});\n\n/**\n * Agent tools for selections and masks (part of `createImageEditorToolProvider`):\n * `get_selection`, `select_region`, `select_by_color`, `modify_selection`, `refine_selection`\n * and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.\n */\nexport function createImageSelectionTools(controller: ImageEditorController): AgentTool[] {\n const getSelection: AgentTool = {\n name: 'get_selection',\n title: 'Read the selection',\n description:\n 'Returns the active selection: whether it is empty, its bounds (natural image pixels), its area in pixels and percent of the image.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read the selection', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: () => summary(controller),\n };\n\n const selectRegion: AgentTool = {\n name: 'select_region',\n title: 'Select a region',\n description:\n 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything (\"all\"), nothing (\"none\") or the inverse of the current selection (\"invert\"). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n shape: { type: 'string', enum: ['rect', 'ellipse', 'all', 'none', 'invert'], description: 'What to select.' },\n x: { type: 'number', description: 'rect / ellipse: left edge in natural image pixels.' },\n y: { type: 'number', description: 'rect / ellipse: top edge in natural image pixels.' },\n width: { type: 'number', description: 'rect / ellipse: width in natural image pixels.' },\n height: { type: 'number', description: 'rect / ellipse: height in natural image pixels.' },\n mode: modeSchema,\n feather: { type: 'number', minimum: 0, description: 'Feather radius in pixels (default 0).' },\n },\n required: ['shape'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select a region', true),\n handler: ({ shape, x, y, width, height, mode, feather }) => {\n const opts = { mode: parseSelectionMode(mode), feather: num(feather, 0) };\n switch (shape) {\n case 'all':\n controller.selectAll();\n break;\n case 'none':\n controller.deselect();\n break;\n case 'invert':\n controller.invertSelection();\n break;\n case 'rect':\n case 'ellipse': {\n const rect = { x: num(x, NaN), y: num(y, NaN), width: num(width, NaN), height: num(height, NaN) };\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) {\n throw new Error('rect and ellipse need numeric x, y, width and height.');\n }\n if (shape === 'rect') controller.selectRect(rect, opts);\n else controller.selectEllipse(rect, opts);\n break;\n }\n default:\n throw new Error(`Unknown shape \"${String(shape)}\". Use rect, ellipse, all, none or invert.`);\n }\n return summary(controller);\n },\n };\n\n const selectByColor: AgentTool = {\n name: 'select_by_color',\n title: 'Select by colour',\n description:\n 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with \"color\" (\"#rrggbb\") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Seed point x in natural image pixels.' },\n y: { type: 'number', description: 'Seed point y in natural image pixels.' },\n color: { type: 'string', description: '#rrggbb: select this colour everywhere instead of a seed point.' },\n tolerance: { type: 'number', minimum: 0, maximum: 255, description: 'Magic wand tolerance (default 32).' },\n contiguous: { type: 'boolean', description: 'Only the connected region around (x, y) (default true).' },\n fuzziness: { type: 'number', minimum: 0, maximum: 200, description: 'Color Range fuzziness (default 40).' },\n sampleMerged: { type: 'boolean', description: 'Sample the adjusted image rather than the source.' },\n sample: {\n type: 'string',\n enum: ['composite', 'active-layer'],\n description: 'composite (default): the whole image; active-layer: only the active pixel layer (see set_active_layer).',\n },\n mode: modeSchema,\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select by colour', true),\n handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, sample, mode }) => {\n const common = {\n mode: parseSelectionMode(mode),\n sampleMerged: sampleMerged === true,\n sample: (sample as 'composite' | 'active-layer' | undefined) ?? 'composite',\n };\n if (typeof color === 'string') {\n await controller.selectColorRange([{ color }], { ...common, fuzziness: num(fuzziness, 40) });\n } else if (typeof x === 'number' && typeof y === 'number') {\n await controller.magicWand({ x, y }, { ...common, tolerance: num(tolerance, 32), contiguous: contiguous !== false });\n } else {\n throw new Error('Pass x and y (magic wand) or color (#rrggbb).');\n }\n return summary(controller);\n },\n };\n\n const modify: AgentTool = {\n name: 'modify_selection',\n title: 'Modify the selection',\n description: 'Select › Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n op: { type: 'string', enum: [...SELECTION_MODIFY_OPS], description: 'How to modify the selection.' },\n radius: { type: 'number', minimum: 0, maximum: 500, description: 'Radius in pixels.' },\n },\n required: ['op', 'radius'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Modify the selection'),\n handler: ({ op, radius }) => {\n if (!(SELECTION_MODIFY_OPS as readonly string[]).includes(op as string)) throw new Error(`Unknown op \"${String(op)}\".`);\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n controller.modifySelection(op as SelectionModifyOp, Math.min(500, Math.max(0, num(radius, 0))));\n return summary(controller);\n },\n };\n\n const refine: AgentTool = {\n name: 'refine_selection',\n title: 'Refine the selection edge',\n description:\n 'Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n radius: { type: 'number', minimum: 0, maximum: 64, description: 'Edge detection radius in pixels.' },\n smartRadius: { type: 'boolean', description: 'Adapt the radius to hard and soft edges.' },\n smooth: { type: 'number', minimum: 0, maximum: 100, description: 'Smooth the edge outline (0-100).' },\n feather: { type: 'number', minimum: 0, maximum: 250, description: 'Feather the edge (pixels).' },\n contrast: { type: 'number', minimum: 0, maximum: 100, description: 'Sharpen the edge transition (0-100).' },\n shiftEdge: { type: 'number', minimum: -100, maximum: 100, description: 'Move the edge out (+) or in (-), in percent.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Refine the selection edge'),\n handler: async (params) => {\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n await controller.refineSelection({\n radius: num(params.radius, 8),\n smartRadius: params.smartRadius === true,\n smooth: num(params.smooth, 0),\n feather: num(params.feather, 0),\n contrast: num(params.contrast, 0),\n shiftEdge: num(params.shiftEdge, 0),\n });\n return summary(controller);\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_selection_as_mask',\n title: 'Mask a layer with the selection',\n description:\n 'Uses the selection as an adjustment layer mask: on layerId, or on a new layer of kind (with optional params), so the adjustment only affects the selected area. density (0-1) and feather (px) soften the mask non-destructively. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Existing adjustment layer to mask.' },\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Kind of a new layer to add and mask (instead of layerId).' },\n params: { type: 'object', description: 'Parameters for the new layer (see list_controls).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0-1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Mask feather in pixels.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Mask a layer with the selection'),\n handler: ({ layerId, kind, params, density, feather }) => {\n const sel = controller.getSelection();\n if (!sel) throw new Error('Nothing is selected.');\n const props = {\n ...(typeof density === 'number' ? { density } : {}),\n ...(typeof feather === 'number' ? { feather } : {}),\n };\n let id: string;\n if (typeof layerId === 'string') {\n if (!controller.getAdjustmentLayer(layerId)) throw new Error(`No adjustment layer \"${layerId}\".`);\n controller.applySelectionAsLayerMask(layerId, props);\n id = layerId;\n } else if (isAdjustmentKind(kind)) {\n id = controller.addAdjustmentLayer({\n kind: kind as AdjustmentKind,\n params: params as never,\n mask: layerMaskFromSelection(sel, props),\n });\n } else {\n throw new Error('Pass layerId (an existing layer) or kind (a new adjustment layer).');\n }\n const layer = controller.getAdjustmentLayer(id)!;\n const { rle: _rle, ...mask } = layer.mask!;\n return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };\n },\n };\n\n return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];\n}\n","import type { CaptureRenderRequest, CaptureSurface } from '../tools/capture.js';\nimport type { ImageEditorController } from './controller';\nimport type { SelectionMask } from './selection/mask';\n\nconst NO_CANVAS = 'Rendering is not available in this environment.';\n\n/** Darkens what is outside the selection (natural pixels), so its coverage reads at a glance. */\nfunction selectionShade(selection: SelectionMask, doc: Document): HTMLCanvasElement | null {\n const canvas = doc.createElement('canvas');\n canvas.width = selection.width;\n canvas.height = selection.height;\n const context = canvas.getContext('2d');\n if (!context) return null;\n const pixels = context.createImageData(selection.width, selection.height);\n for (let i = 0; i < selection.data.length; i++) {\n pixels.data[i * 4 + 3] = Math.round(150 * (1 - selection.data[i] / 255));\n }\n context.putImageData(pixels, 0, 0);\n return canvas;\n}\n\n/** Draws a region of the image editor's source view for `capture_view`. */\nasync function renderImageRegion(controller: ImageEditorController, request: CaptureRenderRequest): Promise<string> {\n const state = controller.getState();\n // Never render above natural size; small regions are enlarged when drawn.\n const k = Math.min(1, request.scale);\n const view = await controller.renderSourceView({ scale: k });\n if (!view) throw new Error('No image is loaded, so there is nothing to capture.');\n if (typeof document === 'undefined') throw new Error(NO_CANVAS);\n const out = document.createElement('canvas');\n out.width = request.width;\n out.height = request.height;\n const context = out.getContext('2d');\n if (!context) throw new Error(NO_CANVAS);\n const { rect } = request;\n // Upscaled regions keep hard pixel edges, so the agent sees real pixels.\n context.imageSmoothingEnabled = request.scale < 1;\n if (request.format === 'jpeg') {\n context.fillStyle = '#ffffff';\n context.fillRect(0, 0, out.width, out.height);\n }\n const kx = view.width / state.imageDimensions.width;\n const ky = view.height / state.imageDimensions.height;\n context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);\n if (request.includeOverlays) {\n const s = request.scale;\n const selection = state.selection;\n if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {\n const shade = selectionShade(selection, document);\n if (shade) context.drawImage(shade, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);\n }\n if (state.crop) {\n context.save();\n context.strokeStyle = '#ffd400';\n context.lineWidth = 2;\n context.setLineDash([6, 4]);\n context.strokeRect((state.crop.x - rect.x) * s, (state.crop.y - rect.y) * s, state.crop.width * s, state.crop.height * s);\n context.restore();\n }\n }\n const url = out.toDataURL(request.format === 'jpeg' ? 'image/jpeg' : 'image/png', request.quality);\n if (!url.startsWith('data:image/') || url.indexOf(',') === url.length - 1) throw new Error(NO_CANVAS);\n return url;\n}\n\n/**\n * The image editor as a capture surface. Units are natural pixels of the source image,\n * the same space as `get_selection` and `select_region`: crop, rotation and flips are not\n * applied (use `render_preview` for the final framing), adjustments, layers and annotations\n * are. Regions: the whole image, the selection's bounds, or a rect. Small regions are\n * enlarged up to 8× with hard pixel edges for close inspection.\n */\nexport function createImageCaptureSurface(controller: ImageEditorController): CaptureSurface {\n const whole = () => {\n const { width, height } = controller.getState().imageDimensions;\n return width > 0 && height > 0 ? { x: 0, y: 0, width, height } : null;\n };\n return {\n units: 'image pixels',\n content: whole,\n selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),\n overlays: 'a shade over unselected pixels and the crop box outline',\n maxScale: 8,\n render: (request) => renderImageRegion(controller, request),\n };\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { BlendMode } from './blend';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport { getRotatedSize, normalizeAngle } from './geometry';\nimport type { ImageHistogram } from './histogram';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Adjustment layer id from get_adjustments (e.g. \"adj_1\").' } as const;\n\nconst round = (n: number, digits = 2) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** Mean and the 1st / 99th percentiles of a 256-bin histogram. */\nfunction binStats(bins: ArrayLike<number>, samples: number) {\n if (!samples) return { mean: null, p1: null, p99: null };\n let sum = 0;\n let seen = 0;\n let p1: number | null = null;\n let p99: number | null = null;\n for (let i = 0; i < bins.length; i++) {\n sum += i * bins[i];\n seen += bins[i];\n if (p1 === null && seen >= samples * 0.01) p1 = i;\n if (p99 === null && seen >= samples * 0.99) p99 = i;\n }\n return { mean: round(sum / samples, 1), p1, p99 };\n}\n\n/** Sums 256 bins into `count` equal buckets. */\nfunction rebin(bins: ArrayLike<number>, count: number): number[] {\n const out = new Array<number>(count).fill(0);\n const width = bins.length / count;\n for (let i = 0; i < bins.length; i++) out[Math.floor(i / width)] += bins[i];\n return out;\n}\n\nfunction histogramSummary(h: ImageHistogram, bins: number | undefined) {\n const pct = (n: number) => (h.samples ? round((n / h.samples) * 100) : 0);\n const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };\n return {\n samples: h.samples,\n transparent: h.transparent,\n clipping: {\n shadowsPercent: { red: pct(h.shadows[0]), green: pct(h.shadows[1]), blue: pct(h.shadows[2]) },\n highlightsPercent: { red: pct(h.highlights[0]), green: pct(h.highlights[1]), blue: pct(h.highlights[2]) }\n },\n stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),\n ...(bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {})\n };\n}\n\n/**\n * Agent tools for the image editor's layers, geometry and history: `get_image_info`,\n * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,\n * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,\n * `undo_image` and `redo_image`. Each write is one undo step and returns what changed.\n */\nexport function createImageEditTools(controller: ImageEditorController): AgentTool[] {\n const layerOrThrow = (id: string) => {\n const layers = controller.getAdjustmentLayers();\n const index = layers.findIndex((l) => l.id === id);\n if (index === -1) {\n const ids = layers.map((l) => l.id);\n throw new Error(`Unknown adjustment layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'There are no adjustment layers.'}`);\n }\n return { layer: layers[index], index };\n };\n const layerJson = (id: string) => JSON.parse(JSON.stringify(controller.getAdjustmentLayer(id)));\n const geometry = () => {\n const s = controller.getState();\n const region = s.crop ?? { x: 0, y: 0, ...s.imageDimensions };\n const out = getRotatedSize(region.width, region.height, s.transform);\n return {\n image: { ...s.imageDimensions },\n crop: s.crop ? { ...s.crop } : null,\n rotation: normalizeAngle(s.transform.rotate),\n flipH: s.transform.flipH,\n flipV: s.transform.flipV,\n output: { width: Math.round(out.width), height: Math.round(out.height) }\n };\n };\n const history = () => ({ canUndo: controller.canUndo, canRedo: controller.canRedo });\n const requireImage = () => {\n const { width, height } = controller.getState().imageDimensions;\n if (!width || !height) throw new Error('No image is loaded. Ask the user to open one first.');\n return { width, height };\n };\n\n const getImageInfo: AgentTool = {\n name: 'get_image_info',\n title: 'Read image geometry',\n description:\n 'Returns the source image size in pixels, the crop box (natural image pixels, or null for the whole image), rotation in degrees, flips, the output size after crop and rotation, the number of adjustment layers and whether undo / redo are available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read image geometry', ...READ },\n handler: () => ({ ...geometry(), layers: controller.getAdjustmentLayers().length, ...history() })\n };\n\n const getHistogram: AgentTool = {\n name: 'get_histogram',\n title: 'Read the histogram',\n description:\n 'Returns the histogram of the last rendered preview (after adjustments and layers): per channel and luminance mean and 1st / 99th percentiles (0-255), and the percentage of pixels clipped to pure black or white per channel. ' +\n 'Use it to judge exposure and clipping numerically. Pass bins for the counts themselves. Fails before the first render: call render_preview first.',\n inputSchema: {\n type: 'object',\n properties: {\n bins: { type: 'integer', enum: [16, 32, 64, 256], description: 'Also return the counts in this many buckets per channel (default: no counts).' }\n },\n additionalProperties: false\n },\n annotations: { title: 'Read the histogram', ...READ },\n handler: ({ bins }) => {\n const h = controller.getHistogram();\n if (!h) throw new Error('No histogram yet: it comes from the rendered preview. Call render_preview, then try again.');\n return histogramSummary(h, bins);\n }\n };\n\n const updateLayer: AgentTool = {\n name: 'update_adjustment_layer',\n title: 'Change an adjustment layer',\n description:\n 'Shows or hides a layer, renames it, or changes its blend mode or opacity, as one undo step. Change its parameters with set_adjustments (\"layers.<id>.<control>\"). Returns the updated layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n enabled: { type: 'boolean', description: 'false hides the layer without deleting it.' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (see list_controls).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1.' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' }\n },\n required: ['layer'],\n additionalProperties: false\n },\n annotations: { title: 'Change an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, enabled, blend, opacity, name }) => {\n layerOrThrow(layer);\n if (enabled === undefined && blend === undefined && opacity === undefined && name === undefined) {\n throw new Error('Nothing to change. Pass enabled, blend, opacity or name.');\n }\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n controller.updateAdjustmentLayer(layer, { enabled, blend: blend as BlendMode | undefined, opacity, name });\n return { layer: layerJson(layer) };\n }\n };\n\n const removeLayer: AgentTool = {\n name: 'remove_adjustment_layer',\n title: 'Delete an adjustment layer',\n description: 'Deletes an adjustment layer and its mask as one undo step. To hide it instead, use update_adjustment_layer with enabled false. Returns the remaining layer ids, bottom to top.',\n inputSchema: { type: 'object', properties: { layer: LAYER_ID }, required: ['layer'], additionalProperties: false },\n annotations: { title: 'Delete an adjustment layer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n handler: ({ layer }) => {\n layerOrThrow(layer);\n controller.removeAdjustmentLayer(layer);\n return { removed: layer, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const moveLayer: AgentTool = {\n name: 'move_adjustment_layer',\n title: 'Reorder an adjustment layer',\n description:\n 'Moves an adjustment layer to a position in the stack (0 = bottom, applied first) as one undo step. Layers apply bottom to top, so order changes the result. Returns the layer ids, bottom to top.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n to: { type: 'integer', minimum: 0, description: 'New index, 0 = bottom; past the top means the top.' }\n },\n required: ['layer', 'to'],\n additionalProperties: false\n },\n annotations: { title: 'Reorder an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, to }) => {\n const { index } = layerOrThrow(layer);\n const top = controller.getAdjustmentLayers().length - 1;\n const target = Math.min(to, top);\n if (target === index) throw new Error(`Layer \"${layer}\" is already at index ${index} (0 = bottom, ${top} = top).`);\n controller.moveAdjustmentLayer(layer, target);\n return { layer, index: target, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const setCrop: AgentTool = {\n name: 'set_crop',\n title: 'Crop the image',\n description:\n 'Sets the crop box in natural image pixels of the un-rotated source (origin top-left; see get_image_info for the size) as one undo step. Non-destructive: reset_crop restores the whole image. Returns the new geometry and output size.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, description: 'Left edge in source pixels.' },\n y: { type: 'number', minimum: 0, description: 'Top edge in source pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in source pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in source pixels.' }\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false\n },\n annotations: { title: 'Crop the image', ...WRITE_IDEMPOTENT },\n handler: ({ x, y, width, height }) => {\n const image = requireImage();\n if (x + width > image.width + 0.5 || y + height > image.height + 0.5) {\n throw new Error(`The crop box (${x}, ${y}, ${width} × ${height}) goes outside the ${image.width} × ${image.height} image.`);\n }\n controller.setCrop({ x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) });\n return geometry();\n }\n };\n\n const resetCrop: AgentTool = {\n name: 'reset_crop',\n title: 'Remove the crop',\n description: 'Removes the crop so the whole image shows again, as one undo step. Returns the new geometry.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Remove the crop', ...WRITE_IDEMPOTENT },\n handler: () => {\n if (!controller.getState().crop) throw new Error('The image is not cropped.');\n controller.resetCrop();\n return geometry();\n }\n };\n\n const setRotation: AgentTool = {\n name: 'set_rotation',\n title: 'Rotate the image',\n description:\n 'Sets the image rotation in degrees clockwise, as one undo step. Quarter turns (90, 180, 270) keep every pixel; other angles straighten the image inside a larger frame with transparent corners, so crop afterwards. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { degrees: { type: 'number', minimum: -360, maximum: 360, description: 'Absolute rotation in degrees clockwise (0 = upright).' } },\n required: ['degrees'],\n additionalProperties: false\n },\n annotations: { title: 'Rotate the image', ...WRITE_IDEMPOTENT },\n handler: ({ degrees }) => {\n requireImage();\n controller.setRotation(degrees);\n return geometry();\n }\n };\n\n const flipImage: AgentTool = {\n name: 'flip_image',\n title: 'Flip the image',\n description: 'Mirrors the image horizontally (left-right) or vertically (top-bottom), as one undo step. Flipping twice restores it. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left-right; vertical mirrors top-bottom.' } },\n required: ['axis'],\n additionalProperties: false\n },\n annotations: { title: 'Flip the image', ...WRITE },\n handler: ({ axis }) => {\n requireImage();\n if (axis === 'horizontal') controller.flipHorizontal();\n else controller.flipVertical();\n return geometry();\n }\n };\n\n const undo: AgentTool = {\n name: 'undo_image',\n title: 'Undo an image edit',\n description: 'Undoes the last image edit (one tool call is one step). Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Undo an image edit', ...WRITE },\n handler: () => {\n if (!controller.undo()) throw new Error('Nothing to undo.');\n return history();\n }\n };\n\n const redo: AgentTool = {\n name: 'redo_image',\n title: 'Redo an image edit',\n description: 'Redoes the last undone image edit. Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Redo an image edit', ...WRITE },\n handler: () => {\n if (!controller.redo()) throw new Error('Nothing to redo.');\n return history();\n }\n };\n\n return [getImageInfo, getHistogram, updateLayer, removeLayer, moveLayer, setCrop, resetCrop, setRotation, flipImage, undo, redo];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { contentBounds } from '../raster/surface';\nimport { findLayer, type ImageLayer, type LayerFilter, type RasterMask } from './layers/model';\nimport { LayerOperationError, requireLayer, type LayerPlacement } from './layers/ops';\nimport { adjustmentKindLabel } from './adjustment-layers';\nimport { BLEND_MODES } from './blend';\nimport type { BlendMode } from './blend';\nimport type { CropBox } from './types';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** A short description of a mask for agents (no pixels). */\nexport function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null {\n if (!mask) return null;\n const out: Record<string, unknown> = {\n kind: mask.source ? 'depth' : 'raster',\n enabled: mask.enabled !== false,\n linked: mask.linked !== false,\n density: mask.density ?? 1,\n feather: mask.feather ?? 0,\n };\n if (!mask.source) {\n out.defaultValue = mask.defaultValue;\n out.paintedBounds = contentBounds(mask.surface);\n }\n return out;\n}\n\n/**\n * A plain description of a layer (and its children) for agents: ids, types, names, document\n * bounds, visibility, opacity, fill, blend, clipping, locks, mask and effects summaries and\n * whether it is active. `boundsOf` gives a pixel layer's document bounds.\n */\nexport function describeLayer(\n layer: ImageLayer,\n activeId: string | null,\n boundsOf: (layer: ImageLayer) => CropBox | null\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n id: layer.id,\n type: layer.type,\n name: layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : ''),\n visible: layer.visible,\n opacity: round(layer.opacity),\n blend: layer.blend,\n clipped: Boolean(layer.clipped),\n locks: layer.locks ? { ...layer.locks } : {},\n mask: maskSummary(layer.mask),\n effects: layer.effects\n ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) }\n : null,\n active: layer.id === activeId,\n };\n switch (layer.type) {\n case 'pixel':\n out.bounds = boundsOf(layer);\n out.offset = { ...layer.offset };\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.filters = (layer.filters ?? []).map((f) => ({ ...f }));\n if (layer.source === 'document') out.source = 'document';\n break;\n case 'adjustment':\n out.kind = layer.kind;\n out.params = JSON.parse(JSON.stringify(layer.params));\n break;\n case 'annotation':\n out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'group':\n out.collapsed = Boolean(layer.collapsed);\n out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));\n break;\n case 'smart':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.contents = { id: layer.contents.id, name: layer.contents.name, kind: layer.contents.kind, width: layer.contents.width, height: layer.contents.height };\n out.smartFilters = (layer.smartFilters?.filters ?? []).map((f) => ({ id: f.id, kind: f.kind, enabled: f.enabled }));\n out.filterMask = maskSummary(layer.smartFilters?.mask);\n break;\n }\n return out;\n}\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers.' } as const;\nconst PLACEMENT = {\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (same group).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer (same group).' },\n parentId: { type: 'string', description: 'Put the layer inside this group (at index, default its top).' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom (with parentId, or at the top level).' },\n} as const;\nconst BLEND = { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' } as const;\n\nfunction placementFrom(args: Record<string, unknown>): LayerPlacement {\n const p: LayerPlacement = {};\n if (typeof args.aboveId === 'string') p.aboveId = args.aboveId;\n if (typeof args.belowId === 'string') p.belowId = args.belowId;\n if (typeof args.parentId === 'string') p.parentId = args.parentId;\n if (typeof args.index === 'number') p.index = args.index;\n return p;\n}\n\n/**\n * Agent tools for the image editor's layer stack: `list_layers`, `set_active_layer`,\n * `add_pixel_layer`, `duplicate_layer`, `remove_layer`, `move_layer`, `set_layer_properties`,\n * `group_layers`, `ungroup_layer`, `merge_down`, `merge_visible`, `flatten_image`, `offset_layer`,\n * `add_layer_mask`, `apply_layer_mask`, `remove_layer_mask`, `set_layer_mask_properties`,\n * `layer_via_copy`, `add_layer_filter`, `update_layer_filter` and `remove_layer_filter`. Each write\n * is one undo step, validates before changing anything and returns what it changed.\n */\nexport function createImageLayerTools(controller: ImageEditorController): AgentTool[] {\n const describe = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n if (!loc) return null;\n return {\n ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)),\n parentId: loc.parent?.id ?? null,\n index: loc.index,\n };\n };\n const exists = (id: unknown, what = 'layerId') => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError(`${what} is required (a layer id from list_layers).`);\n requireLayer(controller.getLayers(), id);\n return id;\n };\n\n const listLayers: AgentTool = {\n name: 'list_layers',\n title: 'List image layers',\n description:\n 'Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List image layers', ...READ },\n handler: () => {\n const s = controller.getState();\n return {\n canvas: { ...s.imageDimensions },\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n };\n },\n };\n\n const setActive: AgentTool = {\n name: 'set_active_layer',\n title: 'Set the active layer',\n description:\n 'Makes a layer active (the base sliders of set_adjustments then edit its develop filter when it is a pixel layer) and chooses whether painting targets its pixels or its mask. View state, not an undo step. Returns the active layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Layer to activate (from list_layers).' },\n target: { type: 'string', enum: ['pixels', 'mask'], description: 'Paint target (default pixels; mask needs a mask).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the active layer', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, target }) => {\n const id = exists(layerId);\n const layer = controller.getLayer(id)!;\n if (target === 'mask' && !layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n controller.setActiveLayer(id, (target as 'pixels' | 'mask' | undefined) ?? 'pixels');\n return { layer: describe(id), activeTarget: controller.getState().activeTarget };\n },\n };\n\n const addPixelLayer: AgentTool = {\n name: 'add_pixel_layer',\n title: 'Add a pixel layer',\n description:\n 'Adds a pixel layer and makes it active: empty (default), filled with color over the canvas, or holding an image (PNG data URL or base64 PNG; other formats and URLs only in a browser) placed at x, y. Goes above the active layer unless aboveId / belowId / parentId / index is given. One undo step. Returns the new layer with its id, bounds and position.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Layer name (default \"Layer N\").' },\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa; omit for an empty layer.' },\n image: { type: 'string', description: 'Image to place: PNG data URL or base64 PNG (or any URL in a browser).' },\n x: { type: 'integer', description: 'Left of the image in document pixels (default 0).' },\n y: { type: 'integer', description: 'Top of the image in document pixels (default 0).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1 (default 1).' },\n blend: BLEND,\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a pixel layer', ...WRITE },\n handler: async (args) => {\n if (args.color !== undefined && args.image !== undefined) throw new LayerOperationError('Give color or image, not both.');\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n const init = {\n name: args.name as string | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n ...placementFrom(args),\n };\n const id =\n typeof args.image === 'string'\n ? await controller.addPixelLayerFromImage(args.image, init)\n : controller.addPixelLayer({ ...init, color: args.color as string | undefined });\n return { layer: describe(id) };\n },\n };\n\n const duplicate: AgentTool = {\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Duplicates a layer (groups with their children) directly above it and makes the copy active. One undo step. Returns the copy with its new id.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId, name }) => ({ layer: describe(controller.duplicateLayer(exists(layerId), name as string | undefined)) }),\n };\n\n const remove: AgentTool = {\n name: 'remove_layer',\n title: 'Delete a layer',\n description:\n 'Deletes a layer (a group with its children). Refused for a layer locked with all. Prefer hiding it (set_layer_properties visible false) when it may be wanted back. One undo step. Returns the removed id and the remaining top-level ids.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayer(id);\n return { removed: id, layers: controller.getLayers().map((l) => l.id) };\n },\n };\n\n const move: AgentTool = {\n name: 'move_layer',\n title: 'Move a layer in the stack',\n description:\n 'Moves a layer above or below another layer (aboveId / belowId, in that layer\\'s group), into a group (parentId, optional index), or to an index at the top level. Give exactly one of aboveId, belowId, parentId, or index alone. One undo step. Returns the new parentId and index.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, ...PLACEMENT },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer in the stack', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n return { layerId: id, ...controller.moveLayer(id, placementFrom(args)) };\n },\n };\n\n const setProps: AgentTool = {\n name: 'set_layer_properties',\n title: 'Set layer properties',\n description:\n 'Changes a layer\\'s name, visible, opacity (0..1), fillOpacity (0..1, pixel layers: fades the pixels but not masks), blend (pass-through is for groups), clipped (clip to the layer below), locks ({pixels, transparency, position, all}) or collapsed (groups). Only the given fields change. Refused (except visible, locks and collapsed) on a layer locked with all. One undo step. Returns the layer after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n name: { type: 'string', description: 'New name.' },\n visible: { type: 'boolean', description: 'Show (true) or hide (false).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n fillOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill 0..1 (pixel layers).' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode; pass-through for groups only.' },\n clipped: { type: 'boolean', description: 'Clip to the nearest unclipped layer below.' },\n locks: {\n type: 'object',\n description: 'Locks to set (missing keys are unlocked).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting and filling.' },\n transparency: { type: 'boolean', description: 'Painting keeps alpha.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'Everything, including properties and deletion.' },\n },\n additionalProperties: false,\n },\n collapsed: { type: 'boolean', description: 'Collapse a group in the layers panel.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer properties', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['name', 'visible', 'opacity', 'fillOpacity', 'blend', 'clipped', 'locks', 'collapsed']) if (args[k] !== undefined) props[k] = args[k];\n if (!Object.keys(props).length) throw new LayerOperationError('Give at least one property to change.');\n controller.setLayerProperties(id, props);\n return { layer: describe(id) };\n },\n };\n\n const group: AgentTool = {\n name: 'group_layers',\n title: 'Group layers',\n description:\n 'Puts sibling layers (same group or top level) into a new pass-through group at the position of the topmost one, keeping their order. One undo step. Returns the group with its id and children.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Ids of the layers to group (from list_layers).' },\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n },\n required: ['layerIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Group layers', ...WRITE },\n handler: ({ layerIds, name }) => {\n const ids = (layerIds as string[]).map((id) => exists(id, 'layerIds[]'));\n return { group: describe(controller.groupLayers(ids, name as string | undefined)) };\n },\n };\n\n const addGroup: AgentTool = {\n name: 'add_group',\n title: 'Add an empty group',\n description:\n 'Adds an empty group (pass-through unless blend is given) above the active layer, or at aboveId / belowId / parentId / index, and makes it active. Move layers into it with move_layer parentId, or use group_layers to group existing layers. One undo step. Returns the group.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (default pass-through).' },\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add an empty group', ...WRITE },\n handler: (args) => {\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n return { group: describe(controller.addGroup({ name: args.name as string | undefined, blend: args.blend as never, ...placementFrom(args) })) };\n },\n };\n\n const ungroup: AgentTool = {\n name: 'ungroup_layer',\n title: 'Ungroup a group',\n description: 'Replaces a group by its children at its position (the group\\'s own opacity, blend and mask are dropped). One undo step. Returns the child ids bottom to top.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'Group id.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Ungroup a group', ...WRITE },\n handler: ({ layerId }) => ({ childIds: controller.ungroupLayer(exists(layerId)) }),\n };\n\n const mergeDown: AgentTool = {\n name: 'merge_down',\n title: 'Merge a layer down',\n description:\n 'Composites a layer (pixel, adjustment, group or annotation) onto the pixel layer directly below it. The lower layer keeps its name, opacity and blend; masks, fill and filters are baked into its pixels. Refused when the layer below is not a pixel layer or is locked, or when the layer is hidden. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'The upper layer to merge down.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.mergeDown(exists(layerId))) }),\n };\n\n const mergeVisible: AgentTool = {\n name: 'merge_visible',\n title: 'Merge visible layers',\n description: 'Composites every visible top-level layer into one normal pixel layer at the position of the lowest of them; hidden layers stay. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Merge visible layers', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.mergeVisible()) }),\n };\n\n const flatten: AgentTool = {\n name: 'flatten_image',\n title: 'Flatten the image',\n description: 'Composites every visible layer into one Background pixel layer and discards hidden layers. Crop, rotation and output opacity stay. One undo step. Returns the Background layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Flatten the image', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.flattenImage()) }),\n };\n\n const offset: AgentTool = {\n name: 'offset_layer',\n title: 'Move a layer\\'s pixels',\n description:\n 'Moves a pixel layer (and its mask) to an absolute offset x, y or by dx, dy, in document pixels (integers). Refused when its position is locked. One undo step. Returns the new offset and document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n x: { type: 'integer', description: 'New horizontal offset in document pixels.' },\n y: { type: 'integer', description: 'New vertical offset in document pixels.' },\n dx: { type: 'integer', description: 'Move right by this many pixels (negative: left).' },\n dy: { type: 'integer', description: 'Move down by this many pixels (negative: up).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer\\'s pixels', ...WRITE },\n handler: ({ layerId, x, y, dx, dy }) => {\n const id = exists(layerId);\n if ([x, y, dx, dy].every((v) => v === undefined)) throw new LayerOperationError('Give x and y, or dx and dy.');\n if ((x !== undefined || y !== undefined) && (dx !== undefined || dy !== undefined)) throw new LayerOperationError('Give x / y or dx / dy, not both.');\n return { layerId: id, ...controller.offsetLayer(id, { x: x as number | undefined, y: y as number | undefined, dx: dx as number | undefined, dy: dy as number | undefined }) };\n },\n };\n\n const addMask: AgentTool = {\n name: 'add_layer_mask',\n title: 'Add a layer mask',\n description:\n 'Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection\\'s coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection'], description: 'What the mask starts as (default reveal-all).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px (default 0).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer mask', ...WRITE },\n handler: ({ layerId, from, density, feather }) => {\n const id = exists(layerId);\n const mask = controller.addLayerMask(id, (from as 'reveal-all' | 'hide-all' | 'selection' | undefined) ?? 'reveal-all', {\n density: density as number | undefined,\n feather: feather as number | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_layer_mask',\n title: 'Apply a layer mask',\n description: \"Bakes a pixel layer's mask into its pixels (alpha × coverage, including density and feather) and removes the mask. Pixel layers only; refused when its pixels are locked. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Apply a layer mask', ...DESTRUCTIVE },\n handler: async ({ layerId }) => {\n const id = exists(layerId);\n await controller.applyLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const removeMask: AgentTool = {\n name: 'remove_layer_mask',\n title: 'Delete a layer mask',\n description: \"Deletes a layer's mask without applying it (the layer shows everywhere again). Disable it instead with set_layer_mask_properties enabled false to keep it. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer mask', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const maskProps: AgentTool = {\n name: 'set_layer_mask_properties',\n title: 'Set layer mask properties',\n description:\n \"Changes a layer mask's density (0..1), feather (px), enabled flag or linked flag; its pixels are kept. An unlinked mask (linked false) stays in place when the layer is moved, transformed or warped; transform it alone with transform_layer target mask. One undo step. Returns the mask summary.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Density 0..1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px.' },\n enabled: { type: 'boolean', description: 'false ignores the mask but keeps it.' },\n linked: { type: 'boolean', description: 'false unlinks the mask from the layer pixels (it stays put when they move or transform); true links it again.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer mask properties', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, density, feather, enabled, linked }) => {\n const id = exists(layerId);\n if ([density, feather, enabled, linked].every((v) => v === undefined)) throw new LayerOperationError('Give density, feather, enabled or linked.');\n const mask = controller.setLayerMaskProps(id, {\n density: density as number | undefined,\n feather: feather as number | undefined,\n enabled: enabled as boolean | undefined,\n linked: linked as boolean | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const viaCopy: AgentTool = {\n name: 'layer_via_copy',\n title: 'New layer from the selection',\n description:\n 'Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Pixel layer to copy from (default the active layer).' },\n cut: { type: 'boolean', description: 'Remove the pixels from the source too (default false).' },\n name: { type: 'string', description: 'Name of the new layer.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'New layer from the selection', ...WRITE },\n handler: ({ layerId, cut, name }) => {\n if (layerId !== undefined) exists(layerId);\n return { layer: describe(controller.layerViaCopy({ layerId: layerId as string | undefined, cut: cut === true, name: name as string | undefined })) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_layer',\n title: 'Rasterize a layer',\n description:\n 'Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer\\'s filters (develop, blur) into its pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a layer', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) }),\n };\n\n const filterArgs = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it.' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n } as const;\n const filterValues = (args: Record<string, unknown>) => {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(filterArgs)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n };\n\n const addFilter: AgentTool = {\n name: 'add_layer_filter',\n title: 'Add a layer filter',\n description:\n 'Adds a live filter to a pixel layer: develop (the base sliders: brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px). One of each kind per layer, applied bottom to top before the mask. One undo step. Returns the layer\\'s filters with their ids.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' }, ...filterArgs },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer filter', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n const filters = controller.addLayerFilter(id, { kind: args.kind as LayerFilter['kind'], ...filterValues(args) } as never);\n return { layerId: id, filters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_layer_filter',\n title: 'Change a layer filter',\n description: \"Changes one filter of a pixel layer (only the given values; the kind cannot change). One undo step. Returns the layer's filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers or add_layer_filter.' }, ...filterArgs },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const changes = filterValues(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n return { layerId: id, filters: controller.updateLayerFilter(id, args.filterId as string, changes) };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_layer_filter',\n title: 'Remove a layer filter',\n description: \"Removes one filter from a pixel layer. One undo step. Returns the layer's remaining filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers.' } },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Remove a layer filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = exists(layerId);\n return { layerId: id, filters: controller.removeLayerFilter(id, filterId as string) };\n },\n };\n\n return [\n listLayers,\n setActive,\n addPixelLayer,\n duplicate,\n remove,\n move,\n setProps,\n group,\n addGroup,\n ungroup,\n mergeDown,\n mergeVisible,\n flatten,\n offset,\n addMask,\n applyMask,\n removeMask,\n maskProps,\n viaCopy,\n rasterize,\n addFilter,\n updateFilter,\n removeFilter,\n ];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES } from './blend';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport {\n effectsMargin,\n isLayerEffectKind,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type LayerEffectKind,\n type LayerEffects,\n} from '../raster/effects/model';\nimport { builtinPattern, builtinPatternIds } from '../raster/effects/patterns';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers (a pixel, annotation or group layer).' } as const;\nconst EFFECT_ID = { type: 'string', description: 'Effect id from list_layer_effects or add_layer_effect.' } as const;\nconst BLEND_IDS = BLEND_MODES.map((m) => m.id).filter((id) => id !== 'pigment');\n\n/** Shared shapes, referenced as `#/$defs/...`. */\nconst DEFS = {\n color: { type: 'string', pattern: '^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', description: 'Colour as #rrggbb.' },\n contour: {\n type: 'object',\n description: 'A transfer curve over the effect coverage: points { input, output } from 0 to 1 (at least 2), sorted by input. Linear is [{0,0},{1,1}].',\n properties: {\n name: { type: 'string', description: 'Display name (e.g. \"Cone\", \"Gaussian\").' },\n points: {\n type: 'array',\n minItems: 2,\n description: 'Curve points.',\n items: {\n type: 'object',\n properties: {\n input: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage in (0..1).' },\n output: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage out (0..1).' },\n corner: { type: 'boolean', description: 'Sharp corner instead of a smooth point (default false).' },\n },\n required: ['input', 'output'],\n additionalProperties: false,\n },\n },\n },\n required: ['name', 'points'],\n additionalProperties: false,\n },\n gradient: {\n type: 'object',\n description: 'A gradient: colour stops, optional opacity stops and its layout.',\n properties: {\n stops: {\n type: 'array',\n minItems: 1,\n description: 'Colour stops { position 0..1, color #rrggbb }.',\n items: {\n type: 'object',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, color: { $ref: '#/$defs/color' } },\n required: ['position', 'color'],\n additionalProperties: false,\n },\n },\n opacityStops: {\n type: 'array',\n description: 'Opacity stops { position 0..1, opacity 0..1 } (default opaque).',\n items: {\n type: 'object',\n properties: {\n position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1.' },\n },\n required: ['position', 'opacity'],\n additionalProperties: false,\n },\n },\n style: { type: 'string', enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'], description: 'Geometry.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees, 90 = bottom to top.' },\n scale: { type: 'number', minimum: 0.1, maximum: 1.5, description: '1 = 100 %.' },\n reverse: { type: 'boolean', description: 'Swap the ends.' },\n align: { type: 'boolean', description: 'Lay out over the layer bounds (true) or the canvas.' },\n offset: {\n type: 'object',\n description: 'Centre offset as fractions of the bounds.',\n properties: { x: { type: 'number', minimum: -1, maximum: 1, description: 'Fraction of the width.' }, y: { type: 'number', minimum: -1, maximum: 1, description: 'Fraction of the height.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n required: ['stops', 'style', 'angle', 'scale', 'reverse', 'align', 'offset'],\n additionalProperties: false,\n },\n phase: {\n type: 'object',\n description: 'Pattern origin offset in px.',\n properties: { x: { type: 'number', description: 'px.' }, y: { type: 'number', description: 'px.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n fill: {\n description: 'What a stroke or glow paints with. Glows take color or gradient only.',\n anyOf: [\n {\n type: 'object',\n properties: { type: { const: 'color', description: 'Solid colour.' }, color: { $ref: '#/$defs/color' } },\n required: ['type', 'color'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: { type: { const: 'gradient', description: 'Gradient.' }, gradient: { $ref: '#/$defs/gradient' } },\n required: ['type', 'gradient'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n type: { const: 'pattern', description: 'Pattern (strokes only).' },\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Rotation in degrees (default 0).' },\n link: { type: 'boolean', description: 'Tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['type', 'patternId', 'scale'],\n additionalProperties: false,\n },\n ],\n },\n} as const;\n\nconst nullable = (schema: Record<string, unknown>, description: string) => ({ anyOf: [schema, { type: 'null' }], description });\n\n/** Every effect setting (each kind takes a subset; the description says which). */\nfunction settingsSchema(forUpdate: boolean): Record<string, unknown> {\n const opt = (schema: Record<string, unknown>, description: string) => (forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description });\n return {\n type: 'object',\n description: forUpdate\n ? 'Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect.'\n : \"Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.\",\n properties: {\n enabled: { type: 'boolean', description: 'All kinds: false keeps the effect but hides it (default true).' },\n blend: { type: 'string', enum: BLEND_IDS, description: 'All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1).' },\n color: { $ref: '#/$defs/color', description: 'drop-shadow, inner-shadow, satin, color-overlay: #rrggbb.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Shadows, satin, bevel-emboss: light direction in degrees, counter-clockwise from 3 o\\'clock (default 120; satin 19).' },\n useGlobalLight: { type: 'boolean', description: 'Shadows, bevel-emboss: follow the document global light instead of angle (default true).' },\n distance: { type: 'number', minimum: 0, maximum: 30000, description: 'Shadows, satin: offset in px (default 5; satin 11).' },\n spread: { type: 'number', minimum: 0, maximum: 1, description: 'drop-shadow, outer-glow: 0..1 of the size made solid (default 0).' },\n choke: { type: 'number', minimum: 0, maximum: 1, description: 'inner-shadow, inner-glow: 0..1 of the size made solid (default 0).' },\n size: { type: 'number', minimum: 0, maximum: 250, description: 'Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3).' },\n contour: opt({ $ref: '#/$defs/contour' }, 'Shadows, glows, satin: coverage contour (default linear).'),\n antiAlias: { type: 'boolean', description: 'Shadows, glows, satin: smooth the contour (default false; satin true).' },\n noise: { type: 'number', minimum: 0, maximum: 1, description: 'Shadows, glows: speckle 0..1 (default 0).' },\n knocksOut: { type: 'boolean', description: 'drop-shadow: the layer hides the shadow beneath its see-through fill (default true).' },\n fill: { $ref: '#/$defs/fill', description: 'outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern).' },\n technique: { type: 'string', enum: ['softer', 'precise', 'smooth', 'chisel-hard', 'chisel-soft'], description: 'Glows: softer | precise (default softer); bevel-emboss: smooth | chisel-hard | chisel-soft (default smooth).' },\n range: { type: 'number', minimum: 0.01, maximum: 1, description: 'Glows: the part of the glow the contour spans (default 0.5).' },\n jitter: { type: 'number', minimum: 0, maximum: 1, description: 'Glows: gradient jitter 0..1 (stored, not rendered; default 0).' },\n source: { type: 'string', enum: ['edge', 'center'], description: 'inner-glow: glow from the edge (default) or the centre.' },\n style: { type: 'string', enum: ['inner-bevel', 'outer-bevel', 'emboss', 'pillow-emboss', 'stroke-emboss'], description: 'bevel-emboss: style (default inner-bevel).' },\n depth: { type: 'number', minimum: 0, maximum: 10, description: 'bevel-emboss: depth, 1 = 100 % (default 1).' },\n direction: { type: 'string', enum: ['up', 'down'], description: 'bevel-emboss: raised (up, default) or sunk (down).' },\n soften: { type: 'number', minimum: 0, maximum: 16, description: 'bevel-emboss: blur of the height in px (default 0).' },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'bevel-emboss: light altitude in degrees (default 30).' },\n glossContour: opt({ $ref: '#/$defs/contour' }, 'bevel-emboss: gloss contour over the shading (default linear).'),\n highlightColor: { $ref: '#/$defs/color', description: 'bevel-emboss: highlight colour (default #ffffff).' },\n highlightBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: highlight mode (default screen).' },\n highlightOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: highlight opacity (default 0.75).' },\n shadowColor: { $ref: '#/$defs/color', description: 'bevel-emboss: shadow colour (default #000000).' },\n shadowBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: shadow mode (default multiply).' },\n shadowOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: shadow opacity (default 0.75).' },\n contourElement: opt(\n {\n type: 'object',\n properties: {\n contour: { $ref: '#/$defs/contour' },\n range: { type: 'number', minimum: 0, maximum: 1, description: 'Stored range (default 0.5).' },\n antiAlias: { type: 'boolean', description: 'Smooth the contour.' },\n },\n required: ['contour', 'range', 'antiAlias'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Contour element, shaping the bevel's height profile (default off).\"\n ),\n texture: opt(\n {\n type: 'object',\n properties: {\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n depth: { type: 'number', minimum: -10, maximum: 10, description: '1 = +100 %; negative carves the pattern in.' },\n invert: { type: 'boolean', description: 'Invert the pattern.' },\n link: { type: 'boolean', description: 'Tile from the layer (default true).' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['patternId', 'scale', 'depth', 'invert'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Texture element, a pattern's luminance as extra height (default off).\"\n ),\n invert: { type: 'boolean', description: 'satin: invert (default true).' },\n gradient: { $ref: '#/$defs/gradient', description: 'gradient-overlay: the gradient (default black to white, 90°).' },\n dither: { type: 'boolean', description: 'gradient-overlay: dither (stored; default false).' },\n patternId: { type: 'string', description: 'pattern-overlay: pattern id from list_layer_effects.patterns (default checker).' },\n name: { type: 'string', description: 'pattern-overlay: pattern display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'pattern-overlay: 1 = 100 % (default 1).' },\n link: { type: 'boolean', description: 'pattern-overlay: tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase', description: 'pattern-overlay: origin offset in px (default 0, 0).' },\n position: { type: 'string', enum: ['outside', 'inside', 'center'], description: 'stroke: where the stroke sits on the edge (default outside).' },\n },\n additionalProperties: false,\n };\n}\n\n/** The patterns pattern effects can use: the document's (from a PSD), then the built-in ones. */\nfunction patternList(controller: ImageEditorController): Array<{ id: string; name: string; width: number; height: number; builtin: boolean }> {\n const doc = controller.getDocumentPatterns().map((p) => ({ id: p.id, name: p.name, width: p.width, height: p.height, builtin: false }));\n const builtins = builtinPatternIds().map((id) => {\n const p = builtinPattern(id)!;\n return { id, name: p.name, width: p.width, height: p.height, builtin: true };\n });\n return [...doc, ...builtins];\n}\n\n/** A layer's effects for agents: the master switch, scale, reach and every effect with its settings. */\nexport function describeLayerEffects(effects: LayerEffects): Record<string, unknown> {\n return {\n enabled: effects.enabled,\n scale: effects.scale ?? 1,\n margin: effectsMargin(effects),\n fromPsd: Boolean(effects.psd),\n effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) })),\n };\n}\n\n/**\n * Agent tools for layer effects: `list_layer_effects`, `add_layer_effect`, `update_layer_effect`,\n * `remove_layer_effect`, `set_layer_effects_enabled` and `set_global_light`. Each write validates\n * every setting first, is one undo step and returns the layer's effects after the change.\n */\nexport function createImageEffectTools(controller: ImageEditorController): AgentTool[] {\n const layerArg = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a layer id from list_layers).');\n requireLayer(controller.getLayers(), id);\n return id;\n };\n const result = (layerId: string, extra: Record<string, unknown> = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });\n\n const list: AgentTool = {\n name: 'list_layer_effects',\n title: 'List layer effects',\n description:\n \"Returns a layer's effects: the master switch (enabled), Scale Effects (scale, 1 = 100 %), margin (px the effects reach beyond the shape), whether they came from a PSD, and every effect with its id, kind, label and all settings. Also returns the document global light and the pattern ids pattern effects can use.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'List layer effects', ...READ },\n handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] }),\n };\n\n const add: AgentTool = {\n name: 'add_layer_effect',\n title: 'Add a layer effect',\n description:\n \"Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: [...LAYER_EFFECT_KINDS], description: 'Effect kind.' },\n settings: settingsSchema(false),\n index: { type: 'integer', minimum: 0, description: \"Position in the layer's effect list (default the end).\" },\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer effect', ...WRITE },\n handler: ({ layerId, kind, settings, index }) => {\n const id = layerArg(layerId);\n if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(', ')}.`);\n const { effect } = controller.addLayerEffect(id, kind as LayerEffectKind, (settings as Record<string, unknown>) ?? {}, index as number | undefined);\n return result(id, { effectId: effect.id });\n },\n };\n\n const update: AgentTool = {\n name: 'update_layer_effect',\n title: 'Change a layer effect',\n description:\n \"Changes settings of one effect (only the given ones; fill, gradient, contour and texture objects are replaced whole, null removes an optional contour or texture). The kind cannot change. Validates everything before changing anything. One undo step. Returns the layer's effects.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: { layerId: LAYER_ID, effectId: EFFECT_ID, settings: settingsSchema(true) },\n required: ['layerId', 'effectId', 'settings'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer effect', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, effectId, settings }) => {\n const id = layerArg(layerId);\n const changes = (settings as Record<string, unknown>) ?? {};\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one setting to change.');\n controller.updateLayerEffect(id, effectId as string, changes);\n return result(id);\n },\n };\n\n const remove: AgentTool = {\n name: 'remove_layer_effect',\n title: 'Remove a layer effect',\n description: \"Removes one effect from a layer (to hide it but keep its settings, set enabled false with update_layer_effect instead). One undo step. Returns the layer's remaining effects.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, effectId: EFFECT_ID }, required: ['layerId', 'effectId'], additionalProperties: false },\n annotations: { title: 'Remove a layer effect', ...DESTRUCTIVE },\n handler: ({ layerId, effectId }) => {\n const id = layerArg(layerId);\n controller.removeLayerEffect(id, effectId as string);\n return result(id);\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_layer_effects_enabled',\n title: 'Switch layer effects on or off',\n description:\n \"Turns all of a layer's effects on or off at once (the master switch; each effect keeps its own enabled flag) and optionally sets Scale Effects, which multiplies every size, distance and pattern scale. One undo step. Returns the layer's effects.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n enabled: { type: 'boolean', description: 'true renders the enabled effects; false hides them all.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'Scale Effects, 1 = 100 % (unchanged when left out).' },\n },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Switch layer effects on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled, scale }) => {\n const id = layerArg(layerId);\n controller.setLayerEffectsEnabled(id, enabled as boolean, scale as number | undefined);\n return result(id);\n },\n };\n\n const light: AgentTool = {\n name: 'set_global_light',\n title: 'Set the global light',\n description:\n 'Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120°, altitude 30°). One undo step. Returns the light.',\n inputSchema: {\n type: 'object',\n properties: {\n angle: { type: 'number', minimum: -360, maximum: 360, description: \"Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out).\" },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'Degrees above the canvas, for bevels (unchanged when left out).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the global light', ...WRITE_IDEMPOTENT },\n handler: ({ angle, altitude }) => {\n if (angle === undefined && altitude === undefined) throw new LayerOperationError('Give angle, altitude or both.');\n const next: { angle?: number; altitude?: number } = {};\n if (angle !== undefined) next.angle = angle as number;\n if (altitude !== undefined) next.altitude = altitude as number;\n return { globalLight: controller.setGlobalLight(next) };\n },\n };\n\n return [list, add, update, remove, setEnabled, light];\n}\n","/**\n * The paint tools' view model, shared by the React and Svelte editors: the brush options bar's\n * controls, Photoshop's shortcuts (B, Shift+B, E, V, [ ], Shift+[ ], number keys, D, X) and a\n * pointer gesture that turns pointer events into strokes (Brush, Mixer Brush, Eraser) or layer\n * moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type MixerSettings } from '../../paint';\n\nconst plain = { kind: 'plain' } as const;\n\n/** Controls of the brush options bar (`hardness`, `opacity`, `flow`, `smoothing` shown in %). */\nexport const BRUSH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'size' | 'hardness' | 'opacity' | 'flow' | 'smoothing' })[] = [\n { id: 'size', label: 'Size', section: 'layer', min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: 'px' },\n { id: 'hardness', label: 'Hardness', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'flow', label: 'Flow', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'smoothing', label: 'Smoothing', section: 'layer', min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** Controls of the Mixer Brush (shown in %, with the brush controls, while the mixer is active). */\nexport const MIXER_OPTION_CONTROLS: readonly (ControlSpec & { id: 'wet' | 'load' | 'mix' })[] = [\n { id: 'wet', label: 'Wet', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'load', label: 'Load', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'mix', label: 'Mix', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** The Mixer Brush's mixing models, for its RGB / Pigment toggle. */\nexport const MIXER_MIXING_OPTIONS: readonly { id: MixerSettings['mixing']; label: string; title: string }[] = [\n { id: 'rgb', label: 'RGB', title: 'Mix colours as RGB averages' },\n { id: 'pigment', label: 'Pigment', title: 'Mix colours like paint: blue and yellow make green' },\n];\n\n/** The paint tools in the toolbar. */\nexport const PAINT_TOOLS = [\n { id: 'move', label: 'Move', shortcut: 'V' },\n { id: 'brush', label: 'Brush', shortcut: 'B' },\n { id: 'mixer', label: 'Mixer', shortcut: 'Shift+B' },\n { id: 'eraser', label: 'Eraser', shortcut: 'E' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => tool === 'move' || tool === 'brush' || tool === 'mixer' || tool === 'eraser';\n\n/** `#rrggbb` of an sRGB triple (0..1), e.g. the Mixer Brush's reservoir. */\nexport function rgbUnitToHex(c: readonly [number, number, number]): string {\n return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('')}`;\n}\n\n/** Photoshop's bracket step for a brush size. */\nfunction sizeStep(size: number): number {\n return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;\n}\n\nexport interface ShortcutEvent {\n key: string;\n shiftKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n /** The event target: shortcuts are ignored while typing in inputs. */\n target?: unknown;\n}\n\nconst isTyping = (t: unknown) => {\n const el = t as { tagName?: string; isContentEditable?: boolean } | null;\n return Boolean(el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'));\n};\n\n/**\n * Applies a paint shortcut. Returns true when it was one (the caller then prevents the default).\n * B / Shift+B (Mixer Brush) / E / V choose the tool; [ and ] change the size, with Shift the hardness by 25 %; 1..9 set the\n * opacity to 10..90 % and 0 to 100 %; D resets the colours to black and white; X swaps them.\n */\nexport function handlePaintShortcut(controller: ImageEditorController, e: ShortcutEvent): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;\n const brush = controller.getState().paint.brush;\n const set = (b: Partial<BrushSettings>) => controller.setPaintOptions({ brush: b });\n const key = e.key;\n switch (key.toLowerCase()) {\n case 'b':\n controller.setTool(e.shiftKey || key === 'B' ? 'mixer' : 'brush');\n return true;\n case 'e':\n controller.setTool('eraser');\n return true;\n case 'v':\n controller.setTool('move');\n return true;\n case 'd':\n controller.resetPaintColors();\n return true;\n case 'x':\n controller.swapPaintColors();\n return true;\n }\n if (key === '[' || key === '{') {\n if (e.shiftKey || key === '{') set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });\n else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });\n return true;\n }\n if (key === ']' || key === '}') {\n if (e.shiftKey || key === '}') set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });\n else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });\n return true;\n }\n if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {\n set({ opacity: key === '0' ? 1 : Number(key) / 10 });\n return true;\n }\n return false;\n}\n\n/** One pointer sample in document pixels. */\nexport interface PaintPointerSample extends ImagePoint {\n /** 0..1; a mouse is 1. */\n pressure: number;\n}\n\n/** Pressure of a pointer event: pens report it; mice report 0.5 while pressed, which means \"full\". */\nexport function pointerPressure(e: { pointerType?: string; pressure?: number }): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * Turns pointer events into strokes (Brush, Mixer Brush, Eraser on the active layer's target) or layer moves\n * (Move on the active pixel layer). `down` returns an error message when the tool cannot act.\n */\nexport class PaintGesture {\n private stroke: PaintStrokeSession | null = null;\n private move: { id: string; start: ImagePoint; origin: { x: number; y: number }; moved: boolean } | null = null;\n /** The last refusal (e.g. \"layer is locked\"), for the editor to show. */\n error: string | null = null;\n\n constructor(private readonly controller: ImageEditorController) {}\n\n get active(): boolean {\n return Boolean(this.stroke || this.move);\n }\n\n down(sample: PaintPointerSample): string | null {\n this.error = null;\n const tool = this.controller.getState().activeTool;\n try {\n if (tool === 'move') {\n const layer = this.controller.getActiveLayer();\n if (!layer) throw new Error('Select a layer to move in the Layers panel.');\n if (layer.type !== 'pixel') throw new Error(`Only pixel layers move; \"${layer.name}\" is a ${layer.type} layer.`);\n if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer \"${layer.name}\" has its position locked.`);\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };\n return null;\n }\n if (tool === 'brush' || tool === 'eraser' || tool === 'mixer') {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([sample]);\n return null;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.stroke = null;\n this.move = null;\n }\n return this.error;\n }\n\n /** Feeds pointer samples (all coalesced events of one move, in order). */\n moveTo(samples: PaintPointerSample[]): void {\n if (!samples.length) return;\n if (this.stroke) {\n this.stroke.push(samples);\n return;\n }\n const m = this.move;\n if (m) {\n const last = samples[samples.length - 1];\n const x = m.origin.x + Math.round(last.x - m.start.x);\n const y = m.origin.y + Math.round(last.y - m.start.y);\n try {\n // The first move of a drag records the undo step; the rest replace it.\n this.controller.offsetLayer(m.id, { x, y }, !m.moved);\n m.moved = true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.move = null;\n }\n }\n }\n\n up(): void {\n if (this.stroke) {\n try {\n this.stroke.end();\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n }\n this.stroke = null;\n this.move = null;\n }\n\n cancel(): void {\n this.stroke?.cancel();\n this.stroke = null;\n this.move = null;\n }\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { MAX_BRUSH_SIZE, MIXING_MODELS, type BrushSettings, type MixerLoad, type MixerSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\nimport { rgbUnitToHex } from './layers/paint-ui';\nimport { LayerOperationError } from './layers/ops';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to paint (default the active layer; see list_layers).' },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask', 'filter-mask'],\n description: \"pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals.\",\n },\n} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush settings; missing fields use the current paint options (default round, hard, 20 px).',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in px (default 20).' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness 0 (soft) .. 1 (hard, default).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness (default 1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity: overlapping dabs never exceed it (default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (default 1).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Point smoothing 0..0.95 (default 0).' },\n pressureSize: { type: 'boolean', description: 'Point pressure scales the size (default true).' },\n pressureOpacity: { type: 'boolean', description: 'Point pressure scales the opacity (default false).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer (default normal). pigment mixes it in like real paint (blue into yellow gives green) instead of laying it over.' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXER = {\n type: 'object',\n description:\n 'Mixer Brush settings (tool mixer only); missing fields use the current mixer options (wet 0.5, load 0.5, mix 0.5, loadEachStroke true, cleanEachStroke true, mixing rgb).',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: 'How much canvas colour each dab picks up, 0 (dry: paints only the reservoir) .. 1 (default 0.5).' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'How much paint the reservoir holds, 0 .. 1 (default 0.5); below 1 it drains per dab, so long strokes run dry.' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"How far each dab's paint moves from the reservoir towards the picked-up colour, 0 .. 1 (default 0.5).\" },\n loadEachStroke: { type: 'boolean', description: 'Refill the reservoir with `color` (or the foreground) before the stroke (default true). Off, the brush keeps what is left of its last load.' },\n cleanEachStroke: { type: 'boolean', description: 'Empty the pickup well after the stroke (default true). Off, the next stroke starts with the colour this one picked up.' },\n mixing: { type: 'string', enum: [...MIXING_MODELS], description: 'rgb (default): RGB averages. pigment: mixes like paint (Kubelka-Munk), so blue dragged through yellow makes green.' },\n },\n additionalProperties: false,\n} as const;\n\n/** A Mixer Brush load as tool output: hex colours and the amount left. */\nexport function mixerLoadResult(load: MixerLoad): { reservoir: string | null; amount: number; pickup: string | null } {\n return {\n reservoir: load.reservoir ? rgbUnitToHex(load.reservoir) : null,\n amount: Math.round(load.amount * 1000) / 1000,\n pickup: load.pickup ? rgbUnitToHex(load.pickup) : null,\n };\n}\n\nfunction readTarget(args: Record<string, unknown>): { layerId?: string; channel?: LayerTarget } {\n return { layerId: args.layerId as string | undefined, channel: args.channel as LayerTarget | undefined };\n}\n\n/**\n * Agent tools for painting: `paint_stroke` (brush, Mixer Brush or eraser along points, on pixels\n * or a mask), `fill_pixels` and `clear_pixels` (Edit › Fill / Clear inside the selection), each\n * one undo step returning the layer, channel and changed bounds; and the Mixer Brush's load:\n * `get_mixer_brush`, `load_mixer_brush` and `clean_mixer_brush` (view state, no undo).\n */\nexport function createImagePaintTools(controller: ImageEditorController): AgentTool[] {\n const paintStroke: AgentTool = {\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke with the brush, smears and blends paint with the Mixer Brush (tool mixer: its reservoir mixes with colour picked up from the layer under each dab, in RGB or pigment mixing), or removes pixels with the eraser, through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask (the mixer paints pixels only). The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count, and for the mixer its mixerLoad after the stroke ({reservoir, amount, pickup}).',\n inputSchema: {\n type: 'object',\n properties: {\n tool: { type: 'string', enum: ['brush', 'eraser', 'mixer'], description: 'brush (default), eraser, or mixer (the Mixer Brush: picks up and blends the layer colour under each dab).' },\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The path in document pixels; one point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in px.' },\n y: { type: 'number', description: 'Document y in px.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure (default 1).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n color: {\n type: 'string',\n description:\n 'Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals. For the mixer, the colour its reservoir is loaded with before the stroke (given, it loads even with loadEachStroke false).',\n },\n brush: BRUSH,\n mixer: MIXER,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (args) => {\n const tool = args.tool as 'brush' | 'eraser' | 'mixer' | undefined;\n if (args.mixer !== undefined && tool !== 'mixer') throw new LayerOperationError('mixer settings apply to the Mixer Brush only; pass tool \"mixer\", or drop mixer.');\n const result = controller.paintStroke({\n tool,\n points: args.points as StrokePoint[],\n color: args.color as string | undefined,\n brush: args.brush as Partial<BrushSettings> | undefined,\n mixer: args.mixer as Partial<MixerSettings> | undefined,\n ...readTarget(args),\n });\n return result.mixerLoad ? { ...result, mixerLoad: mixerLoadResult(result.mixerLoad) } : result;\n },\n };\n\n const getMixer: AgentTool = {\n name: 'get_mixer_brush',\n title: 'Get the Mixer Brush',\n description:\n \"Reads the Mixer Brush: its settings (wet, load, mix, loadEachStroke, cleanEachStroke, mixing) and what it carries now (reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null). Call it before a paint_stroke with tool mixer and loadEachStroke false, to know the brush's starting paint.\",\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get the Mixer Brush', ...READ },\n handler: () => {\n const { mixer, mixerLoad } = controller.getState().paint;\n return { settings: { ...mixer }, load: mixerLoadResult(mixerLoad) };\n },\n };\n\n const loadMixerTool: AgentTool = {\n name: 'load_mixer_brush',\n title: 'Load the Mixer Brush',\n description:\n 'Fills the Mixer Brush reservoir with a colour (amount 1); the picked-up colour is kept. View state, not an undo step. Use it with loadEachStroke false to carry one load across several paint_stroke calls. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { color: { type: 'string', description: 'Paint colour #rrggbb (default the foreground).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Load the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(controller.loadMixerBrush(args.color as string | undefined)),\n };\n\n const cleanMixerTool: AgentTool = {\n name: 'clean_mixer_brush',\n title: 'Clean the Mixer Brush',\n description:\n 'Cleans the Mixer Brush: empties the pickup well so the next stroke starts without canvas colour; with emptyReservoir also drops the loaded paint (the brush then only smears). View state, not an undo step. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { emptyReservoir: { type: 'boolean', description: 'Also drop the reservoir (default false).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Clean the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(args.emptyReservoir === true ? controller.emptyMixerBrush() : controller.cleanMixerBrush()),\n };\n\n const fill: AgentTool = {\n name: 'fill_pixels',\n title: 'Fill pixels',\n description:\n 'Edit › Fill: fills the selection (or the whole canvas without one, or with useSelection false) on a pixel layer or a mask with a colour (default the foreground) at opacity, with a blend mode. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa (default the foreground).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill opacity 0..1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal). pigment mixes the colour into the pixels like paint, opacity being the share of new paint.' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill pixels', ...WRITE },\n handler: (args) =>\n controller.fillPixels({\n color: args.color as string | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n useSelection: args.useSelection as boolean | undefined,\n ...readTarget(args),\n }),\n };\n\n const clear: AgentTool = {\n name: 'clear_pixels',\n title: 'Clear pixels',\n description:\n 'Edit › Clear: removes the selected pixels of a pixel layer (or, without a selection or with useSelection false, all of them); on a transparency-locked layer or a mask it fills with the background colour instead. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'How much to clear 0..1 (default 1).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Clear pixels', ...DESTRUCTIVE },\n handler: (args) =>\n controller.clearPixels({ opacity: args.opacity as number | undefined, useSelection: args.useSelection as boolean | undefined, ...readTarget(args) }),\n };\n\n return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { describeLayer } from './layer-tools';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { PsdError } from './psd/binary';\nimport { importPsd } from './psd/import';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst DATA = {\n type: 'string',\n minLength: 1,\n description: 'The PSD or PSB file as base64 (a `data:` URL prefix is accepted).',\n} as const;\n\n/** Base64 (or a data URL) → bytes, refusing anything that is not base64 text. */\nexport function psdBytesFromBase64(data: unknown): Uint8Array {\n if (typeof data !== 'string' || !data.trim()) throw new PsdError('data is required: the PSD file as base64.');\n const body = data.trim().replace(/^data:[^,]*;base64,/i, '');\n if (!/^[A-Za-z0-9+/=_\\s-]+$/.test(body)) throw new PsdError('data is not base64; send the PSD file bytes base64-encoded.');\n const bytes = base64ToBytes(body);\n if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);\n return bytes;\n}\n\n/**\n * Agent tools for Photoshop files: `inspect_psd` (what a file holds and how it would map),\n * `import_psd` (open it as the document) and `export_psd` (the document as a PSD).\n */\nexport function createImagePsdTools(controller: ImageEditorController): AgentTool[] {\n const inspect: AgentTool = {\n name: 'inspect_psd',\n title: 'Inspect a PSD',\n description:\n 'Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Inspect a PSD', ...READ },\n handler: ({ data }) => {\n const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });\n return {\n width: res.width,\n height: res.height,\n ...res.info,\n resolution: res.resolution ?? null,\n layers: res.summary.map(({ id: _id, ...s }) => s),\n warnings: res.warnings,\n };\n },\n };\n\n const open: AgentTool = {\n name: 'import_psd',\n title: 'Open a PSD as the document',\n description:\n 'Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; type, smart-object, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Open a PSD as the document', ...DESTRUCTIVE },\n handler: ({ data }) => {\n const res = controller.loadPsd(psdBytesFromBase64(data));\n const s = controller.getState();\n return {\n width: res.width,\n height: res.height,\n layerIds: res.layerIds,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n activeLayerId: s.activeLayerId,\n warnings: res.warnings,\n };\n },\n };\n\n const save: AgentTool = {\n name: 'export_psd',\n title: 'Export the document as a PSD',\n description:\n 'Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor\\'s composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Export the document as a PSD', ...READ },\n handler: async () => {\n const res = await controller.toPsd();\n return {\n data: bytesToBase64(res.bytes),\n mimeType: 'image/vnd.adobe.photoshop',\n size: res.bytes.length,\n psb: res.psb,\n width: res.width,\n height: res.height,\n warnings: res.warnings,\n };\n },\n };\n\n return [inspect, open, save];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { LayerOperationError } from './layers/ops';\nimport { FREE_TRANSFORM_CONTENTS, type FreeTransformContent, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n TRANSFORM_ANCHORS,\n WARP_STYLES,\n anchorPoint,\n isTransformAnchor,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpStyle,\n} from './transform';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { identityWarpMesh, warpMeshColumns, warpMeshRows } from '../geom/warp';\nimport type { LayerTarget } from './layers/model';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to transform (from list_layers; default the active layer).' },\n content: {\n type: 'string',\n enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== 'selection-outline'),\n description:\n 'What moves: layer (all its pixels with a linked mask), selection (only the selected pixels, committed back into the layer; the outline moves too), selection-copy (Layer via Copy: the selected pixels on a new layer above, then transformed; the original stays) or auto (default: selection when something is selected, else layer).',\n },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask'],\n description: 'pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels).',\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop’s Bicubic).' },\n} as const;\n\nconst GEOMETRY = {\n scaleX: { type: 'number', description: 'Width in percent of the frame (default 100; negative flips horizontally).' },\n scaleY: { type: 'number', description: 'Height in percent of the frame (default 100; negative flips vertically).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise (default 0).' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees: vertical edges lean right for positive angles (default 0).' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees: horizontal edges lean down for positive angles (default 0).' },\n dx: { type: 'number', description: 'Move right after scaling and rotating, in document pixels (default 0).' },\n dy: { type: 'number', description: 'Move down after scaling and rotating, in document pixels (default 0).' },\n reference: {\n description: 'Fixed point of scale, skew and rotate: an anchor of the frame (top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right; default center) or a document point {x, y}.',\n anyOf: [{ type: 'string', enum: TRANSFORM_ANCHORS }, POINT],\n },\n matrix: {\n type: 'array',\n items: { type: 'number' },\n minItems: 6,\n maxItems: 6,\n description: \"Instead of the numeric fields: an affine matrix [a, b, c, d, e, f] in document pixels (x' = a·x + c·y + e, y' = b·x + d·y + f).\",\n },\n corners: {\n type: 'array',\n items: POINT,\n minItems: 4,\n maxItems: 4,\n description:\n 'Instead of the numeric fields: where the frame’s corners go, [top-left, top-right, bottom-right, bottom-left] in document pixels (Distort; for Perspective move a pair of corners symmetrically). The quad must stay convex. Get the frame and its corners from get_transform_frame.',\n },\n} as const;\n\nconst NUMERIC = ['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'dx', 'dy', 'reference'] as const;\n\nfunction geometryFrom(args: Record<string, unknown>): { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams } {\n const numeric = NUMERIC.filter((k) => args[k] !== undefined);\n const ways = [numeric.length > 0, args.matrix !== undefined, args.corners !== undefined].filter(Boolean).length;\n if (ways === 0) throw new LayerOperationError('Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.');\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: numeric fields, matrix or corners, not a mix.');\n if (args.matrix !== undefined) return { matrix: args.matrix as unknown as AffineMatrix };\n if (args.corners !== undefined) return { corners: args.corners as unknown as Quad };\n const params: TransformParams = {};\n for (const k of NUMERIC) if (args[k] !== undefined) (params as Record<string, unknown>)[k] = args[k];\n if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the layer to a line).');\n if (typeof params.reference === 'string' && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference \"${params.reference}\". Use ${TRANSFORM_ANCHORS.join(', ')} or {x, y}.`);\n return { params };\n}\n\nfunction targetFrom(args: Record<string, unknown>): FreeTransformInit {\n const init: FreeTransformInit = {};\n if (typeof args.layerId === 'string') init.layerId = args.layerId;\n if (args.content !== undefined) init.content = args.content as FreeTransformContent;\n init.channel = (args.channel as LayerTarget | undefined) ?? 'pixels';\n if (args.interpolation !== undefined) init.interpolation = args.interpolation as Interpolation;\n return init;\n}\n\nfunction summary(res: FreeTransformResult) {\n return {\n layerId: res.layerId,\n content: res.content,\n moved: res.moved,\n changed: res.changed,\n frame: { ...res.box },\n corners: res.corners.map(roundPoint),\n bounds: res.bounds,\n };\n}\n\n/**\n * Agent tools for Free Transform in the image editor: `get_transform_frame`, `transform_layer`\n * (scale, rotate, skew and move as numbers, a matrix, or distort / perspective corners),\n * `warp_layer` (a preset or a Bézier mesh), `transform_selection` (the outline only),\n * `flip_layer` and `rotate_layer_90`. Every write is one undo step, resamples tile by tile and\n * returns the new bounds.\n */\nexport function createImageTransformTools(controller: ImageEditorController): AgentTool[] {\n const getFrame: AgentTool = {\n name: 'get_transform_frame',\n title: 'Get the Free Transform frame',\n description:\n 'Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. For a smart object (smartObject true) the frame is the contents’ rectangle in contents pixels and the corners are where they land now. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: TARGET.layerId,\n content: { type: 'string', enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET.content.description} selection-outline frames the selection outline alone.` },\n channel: TARGET.channel,\n meshColumns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns of an identity warp mesh to return (omit for none).' },\n meshRows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows of an identity warp mesh to return (default meshColumns).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Get the Free Transform frame', ...READ },\n handler: (args) => {\n const init = targetFrom(args);\n // A copy's frame is the selection's; framing it must not add the layer.\n if (init.content === 'selection-copy') init.content = 'selection';\n const s = controller.beginFreeTransform(init);\n const out: Record<string, unknown> = {\n layerId: s.layerId,\n content: args.content === 'selection-copy' ? 'selection-copy' : s.content,\n moved: [...s.moves],\n frame: { ...s.box },\n corners: s.startGeometry.corners.map(roundPoint),\n center: roundPoint(anchorPoint(s.startGeometry, 'center')),\n };\n if (s.smartObject) out.smartObject = true;\n s.cancel();\n if (args.meshColumns !== undefined) {\n const cols = args.meshColumns as number;\n const rows = (args.meshRows as number | undefined) ?? cols;\n const mesh = identityWarpMesh(s.box, cols, rows);\n out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };\n }\n return out;\n },\n };\n\n const transformLayer: AgentTool = {\n name: 'transform_layer',\n title: 'Free Transform a layer',\n description:\n 'Edit › Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop’s options bar, about a reference point), a matrix, or the frame’s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: { ...TARGET, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Free Transform a layer', ...WRITE },\n handler: (args) => summary(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) })),\n };\n\n const warpLayer: AgentTool = {\n name: 'warp_layer',\n title: 'Warp a layer',\n description:\n 'Edit › Transform › Warp: bends a pixel layer (with its linked mask), the selected pixels, a copy of them, or an unlinked mask through a preset (style with bend and distortions in percent) or a Bézier mesh over the frame (get the frame and an identity mesh to edit from get_transform_frame with meshColumns). Give style or mesh, not both. One undo step. Returns what moved and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n ...TARGET,\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset warp (Photoshop’s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist.' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Preset bend in percent, −100..100 (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Preset horizontal distortion in percent, −100..100 (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Preset vertical distortion in percent, −100..100 (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend along the horizontal (default) or vertical axis.' },\n mesh: {\n type: 'object',\n description: 'A custom warp: patch columns and rows, and the (3·columns + 1) × (3·rows + 1) control points row by row in document pixels (anchors where both indices are multiples of 3, Bézier handles between). Start from get_transform_frame’s identity mesh.',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'Control points, row by row, document pixels.' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Warp a layer', ...WRITE },\n handler: (args) => {\n const hasPreset = ['style', 'bend', 'horizontal', 'vertical', 'orientation'].some((k) => args[k] !== undefined);\n if (hasPreset && args.mesh !== undefined) throw new LayerOperationError('Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.');\n if (!hasPreset && args.mesh === undefined) throw new LayerOperationError('Give a preset style or a mesh.');\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' };\n if (hasPreset) {\n if (args.style === undefined) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(', ')}.`);\n warp = {\n style: args.style as WarpStyle,\n bend: (args.bend as number | undefined) ?? 50,\n horizontal: args.horizontal as number | undefined,\n vertical: args.vertical as number | undefined,\n orientation: args.orientation as 'horizontal' | 'vertical' | undefined,\n };\n } else {\n const m = args.mesh as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n return summary(controller.transformLayer({ ...targetFrom(args), warp }));\n },\n };\n\n const transformSelection: AgentTool = {\n name: 'transform_selection',\n title: 'Transform the selection outline',\n description:\n 'Select › Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection’s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.',\n inputSchema: {\n type: 'object',\n properties: { interpolation: { ...TARGET.interpolation, description: 'Resampling of soft edges: nearest, bilinear or bicubic (default).' }, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Transform the selection outline', ...WRITE },\n handler: (args) => {\n const res = controller.transformLayer({ content: 'selection-outline', interpolation: args.interpolation as Interpolation | undefined, ...geometryFrom(args) });\n return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };\n },\n };\n\n const flipLayer: AgentTool = {\n name: 'flip_layer',\n title: 'Flip a layer',\n description:\n 'Edit › Transform › Flip Horizontal or Vertical: mirrors a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (no resampling). To flip the whole image use flip_image. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left ↔ right, vertical top ↔ bottom.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['axis'],\n additionalProperties: false,\n },\n annotations: { title: 'Flip a layer', ...WRITE },\n handler: (args) => summary(controller.flipLayer(args.axis as 'horizontal' | 'vertical', targetFrom(args))),\n };\n\n const rotate90: AgentTool = {\n name: 'rotate_layer_90',\n title: 'Rotate a layer by quarter turns',\n description:\n 'Edit › Transform › Rotate 90° / 180°: turns a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (pixels land on whole pixels; no resampling). To rotate the whole image use set_rotation. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n turn: { type: 'integer', enum: [90, -90, 180], description: 'Degrees: 90 clockwise, -90 counter-clockwise, or 180.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['turn'],\n additionalProperties: false,\n },\n annotations: { title: 'Rotate a layer by quarter turns', ...WRITE },\n handler: (args) => summary(controller.rotateLayer90(args.turn as 90 | -90 | 180, targetFrom(args))),\n };\n\n return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];\n}\n\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { bytesToBase64 } from '../raster/png';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { WARP_STYLES, type AffineMatrix, type Quad, type WarpMesh, type WarpStyle } from './transform';\nimport { BLEND_MODES } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport { requireSmartObject } from './layers/smart';\nimport { describeLayer, maskSummary } from './layer-tools';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst LAYER_ID = { type: 'string', description: 'Smart object layer id from list_layers (type smart).' } as const;\nconst FILTER_ID = { type: 'string', description: 'Smart filter id from get_smart_object or add_smart_filter.' } as const;\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\nconst FILTER_BLENDS = BLEND_MODES.map((m) => m.id).filter((m) => m !== 'pigment');\n\nconst FILTER_SETTINGS = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it (the eye in the Layers panel; default true).' },\n blend: { type: 'string', enum: FILTER_BLENDS, description: 'How the filter’s result blends with the result below it (Blending Options; default normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of the filter’s result over the result below it, 0..1 (default 1).' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 blue to yellow (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n} as const;\n\nfunction settingsFrom(args: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(FILTER_SETTINGS)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n}\n\n/** A PNG (data URL or base64) as a data URL. */\nfunction imageArg(image: unknown): string {\n if (typeof image !== 'string' || !image.trim()) throw new LayerOperationError('image is required: a PNG as a data URL or base64 (other formats need a browser).');\n return image.trim();\n}\n\n/**\n * Agent tools for smart objects in the image editor: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, and the smart filter stack: `add_smart_filter`,\n * `update_smart_filter`, `remove_smart_filter`, `move_smart_filter`, `set_smart_filters_enabled`,\n * `set_smart_filter_mask`. Every write is one undo step, validates first and returns what changed.\n */\nexport function createImageSmartObjectTools(controller: ImageEditorController): AgentTool[] {\n const smartId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a smart object id from list_layers).');\n requireSmartObject(controller.getLayers(), id);\n return id;\n };\n const info = (id: string) => {\n const i = controller.getSmartObject(id);\n return {\n ...i,\n placement: { ...i.placement, corners: i.placement.corners.map(roundPoint), center: roundPoint(i.placement.center), scaleX: round(i.placement.scaleX), scaleY: round(i.placement.scaleY), rotate: round(i.placement.rotate), skewX: round(i.placement.skewX) },\n };\n };\n const layerSummary = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;\n };\n\n const getSmart: AgentTool = {\n name: 'get_smart_object',\n title: 'Read a smart object',\n description:\n 'Returns a smart object’s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents’ own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a smart object', ...READ },\n handler: ({ layerId }) => info(smartId(layerId)),\n };\n\n const convert: AgentTool = {\n name: 'convert_to_smart_object',\n title: 'Convert to Smart Object',\n description:\n 'Layer › Smart Objects › Convert to Smart Object: sibling layers become one smart object at the topmost one’s position, so later transforms and filters are non-destructive. One pixel layer keeps its id, name, opacity, blend, fill, mask and effects, and its filters become smart filters (it looks exactly the same); several layers, a group or an adjustment become a nested layer document (edit it with edit_smart_object_contents). One undo step. Returns the new layer and its contents.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Sibling layer ids from list_layers (default the active layer).' },\n name: { type: 'string', description: 'Name of the smart object (default the top layer’s name).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Convert to Smart Object', ...WRITE },\n handler: ({ layerIds, name }) => {\n for (const id of (layerIds as string[] | undefined) ?? []) requireLayer(controller.getLayers(), id);\n const id = controller.convertToSmartObject(layerIds as string[] | undefined, { name: name as string | undefined });\n return { layer: layerSummary(id), smartObject: info(id) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_smart_object',\n title: 'Rasterize a smart object',\n description:\n 'Layer › Smart Objects › Rasterize: the smart object becomes a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend and fill). The contents, placement and filters are gone, so later transforms resample the pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a smart object', ...DESTRUCTIVE },\n handler: ({ layerId }) => ({ layer: layerSummary(controller.rasterizeSmartObject(smartId(layerId))) }),\n };\n\n const setTransform: AgentTool = {\n name: 'set_smart_object_transform',\n title: 'Set a smart object’s transform',\n description:\n 'Sets where a smart object’s contents land, absolutely (not relative to the current transform), and renders it again from the contents, so nothing degrades however often it is transformed. Give at most one of: corners (Distort / Perspective), matrix (contents pixels → document), or the numbers scaleX / scaleY (percent of the contents’ own size), rotate, skewX, skewY and x, y (where the contents’ centre goes; unset numbers keep their current values). warp sets a preset or a mesh in contents pixels (null removes it); with nothing else given the corners stay. A linked mask follows a warp-free change. For a relative nudge use transform_layer. One undo step. Returns the placement and bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n corners: { type: 'array', items: POINT, minItems: 4, maxItems: 4, description: 'Where the contents’ corners land: [top-left, top-right, bottom-right, bottom-left] in document pixels. The quad must stay convex.' },\n matrix: { type: 'array', items: { type: 'number' }, minItems: 6, maxItems: 6, description: \"Affine map from contents pixels to document pixels [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\" },\n scaleX: { type: 'number', description: 'Width in percent of the contents’ own width (negative flips).' },\n scaleY: { type: 'number', description: 'Height in percent of the contents’ own height (negative flips).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise.' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees.' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees.' },\n x: { type: 'number', description: 'Document x of the contents’ centre (default where it is now).' },\n y: { type: 'number', description: 'Document y of the contents’ centre (default where it is now).' },\n warp: {\n description: 'A warp over the contents: a preset {style, bend, horizontal, vertical, orientation}, a mesh {columns, rows, points} in contents pixels, or null to remove the warp. Omit to keep the current warp.',\n anyOf: [\n { type: 'null' },\n {\n type: 'object',\n properties: {\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset (Photoshop’s menu).' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Bend in percent (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Horizontal distortion in percent (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Vertical distortion in percent (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend axis (default horizontal).' },\n },\n required: ['style'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'The (3·columns + 1) × (3·rows + 1) control points row by row, in contents pixels (0..width × 0..height is no warp).' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n ],\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest, bilinear or bicubic (default: as before, bicubic for new smart objects).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a smart object’s transform', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' } | null | undefined;\n const w = args.warp as Record<string, unknown> | null | undefined;\n if (w === null) warp = null;\n else if (w && 'style' in w) warp = { style: w.style as WarpStyle, bend: (w.bend as number | undefined) ?? 50, horizontal: w.horizontal as number | undefined, vertical: w.vertical as number | undefined, orientation: w.orientation as 'horizontal' | 'vertical' | undefined };\n else if (w) {\n const m = w as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n const keys = ['corners', 'matrix', 'scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y', 'warp', 'interpolation'];\n if (!keys.some((k) => args[k] !== undefined)) throw new LayerOperationError(`Give something to set: ${keys.join(', ')}.`);\n const res = controller.setSmartObjectTransform(id, {\n corners: args.corners as Quad | undefined,\n matrix: args.matrix as AffineMatrix | undefined,\n scaleX: args.scaleX as number | undefined,\n scaleY: args.scaleY as number | undefined,\n rotate: args.rotate as number | undefined,\n skewX: args.skewX as number | undefined,\n skewY: args.skewY as number | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n warp,\n interpolation: args.interpolation as Interpolation | undefined,\n });\n return { layerId: id, placement: info(id).placement, masksMoved: res.masksMoved, bounds: controller.getLayerBounds(id) };\n },\n };\n\n const replace: AgentTool = {\n name: 'replace_smart_object_contents',\n title: 'Replace a smart object’s contents',\n description:\n 'Layer › Smart Objects › Replace Contents: new raster contents from an image for the smart object and every linked instance (same contents id). Each instance keeps its scale and rotation about the contents’ centre and its smart filters, and renders again. One undo step. Returns the contents id, the new size and the layers that changed.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n image: { type: 'string', description: 'The new contents: a PNG as a data URL or base64 (other formats and URLs need a browser).' },\n name: { type: 'string', description: 'File name of the contents (default the old name with .png).' },\n },\n required: ['layerId', 'image'],\n additionalProperties: false,\n },\n annotations: { title: 'Replace a smart object’s contents', ...DESTRUCTIVE },\n handler: async ({ layerId, image, name }) => controller.replaceSmartObjectContentsFromImage(smartId(layerId), imageArg(image), name as string | undefined),\n };\n\n const exportContents: AgentTool = {\n name: 'export_smart_object_contents',\n title: 'Export a smart object’s contents',\n description:\n 'Layer › Smart Objects › Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG…), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Export a smart object’s contents', ...READ },\n handler: ({ layerId }) => {\n const f = controller.exportSmartObjectContents(smartId(layerId));\n return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };\n },\n };\n\n const copy: AgentTool = {\n name: 'copy_smart_object',\n title: 'Copy a smart object',\n description:\n 'Duplicates a smart object directly above it. linked true (Duplicate Layer) shares the contents, so editing or replacing them changes both; linked false (Layer › Smart Objects › New Smart Object via Copy) gives the copy its own contents. Placement, filters, mask and effects are copied either way. One undo step. Returns the copy.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n linked: { type: 'boolean', description: 'true shares the contents with the original; false (default) makes independent contents.' },\n name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Copy a smart object', ...WRITE },\n handler: ({ layerId, linked, name }) => {\n const id = controller.copySmartObject(smartId(layerId), linked === true, name as string | undefined);\n return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };\n },\n };\n\n const editContents: AgentTool = {\n name: 'edit_smart_object_contents',\n title: 'Edit a smart object’s contents',\n description:\n 'Layer › Smart Objects › Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer \"Layer 0\") with their own undo history. Every other tool (list_layers, paint, adjustments…) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents’ size and layers.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Edit a smart object’s contents', ...WRITE },\n handler: ({ layerId }) => {\n const res = controller.editSmartObjectContents(smartId(layerId));\n const s = controller.getState();\n return { ...res, editing: controller.getEditingContents(), layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))) };\n },\n };\n\n const closeContents: AgentTool = {\n name: 'close_smart_object_contents',\n title: 'Close a smart object’s contents',\n description:\n 'Closes the contents opened with edit_smart_object_contents. save true (default) writes the edits back: the smart object and every linked instance render again, as one undo step in the document; save false drops them. Returns the smart object, whether it changed, the layers that changed and what is still open.',\n inputSchema: {\n type: 'object',\n properties: { save: { type: 'boolean', description: 'true (default) saves the edits into the smart object; false discards them.' } },\n additionalProperties: false,\n },\n annotations: { title: 'Close a smart object’s contents', ...WRITE },\n handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() }),\n };\n\n const addFilter: AgentTool = {\n name: 'add_smart_filter',\n title: 'Add a smart filter',\n description:\n 'Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' },\n index: { type: 'integer', minimum: 0, description: 'Position in the stack, 0 = bottom, applied first (default the top).' },\n ...FILTER_SETTINGS,\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a smart filter', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const filter = controller.addSmartFilter(id, { kind: args.kind as string, ...settingsFrom(args) }, args.index as number | undefined);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_smart_filter',\n title: 'Change a smart filter',\n description:\n 'Changes one smart filter: its settings, enabled, blend or opacity (only the given values; the kind cannot change). A Photoshop filter kept from a PSD (kind psd) only takes enabled, blend and opacity. One undo step. Returns the filter and the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, ...FILTER_SETTINGS },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a smart filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n const changes = settingsFrom(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n const filter = controller.updateSmartFilter(id, args.filterId as string, changes);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_smart_filter',\n title: 'Remove a smart filter',\n description: 'Removes one smart filter (the filter mask goes with the last one). One undo step. Returns the remaining stack.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, filterId: FILTER_ID }, required: ['layerId', 'filterId'], additionalProperties: false },\n annotations: { title: 'Remove a smart filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = smartId(layerId);\n controller.removeSmartFilter(id, filterId as string);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const moveFilter: AgentTool = {\n name: 'move_smart_filter',\n title: 'Reorder a smart filter',\n description: 'Moves one smart filter to a position in the stack (0 = bottom, applied first), as dragging it in the Layers panel. One undo step. Returns the stack in its new order.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, index: { type: 'integer', minimum: 0, description: 'New position, 0 = bottom (applied first).' } },\n required: ['layerId', 'filterId', 'index'],\n additionalProperties: false,\n },\n annotations: { title: 'Reorder a smart filter', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, filterId, index }) => {\n const id = smartId(layerId);\n controller.moveSmartFilter(id, filterId as string, index as number);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_smart_filters_enabled',\n title: 'Turn smart filters on or off',\n description: 'Layer › Smart Filter › Disable / Enable Smart Filters: switches the whole stack; each filter keeps its own enabled flag. One undo step. Returns the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, enabled: { type: 'boolean', description: 'false hides every smart filter; true shows them again.' } },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Turn smart filters on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled }) => {\n const id = smartId(layerId);\n controller.setSmartFiltersEnabled(id, enabled === true);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setMask: AgentTool = {\n name: 'set_smart_filter_mask',\n title: 'Set the smart filter mask',\n description:\n 'Sets the one filter mask of a smart object’s smart filters, which says where the filtered result shows over the unfiltered contents: from reveal-all (white), hide-all (black), selection (the selection’s coverage) or none (remove it); keep changes only its properties. Paint it with paint_stroke channel filter-mask. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection', 'none', 'keep'], description: 'What the mask starts from; none removes it; keep only changes the properties below.' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'How much hidden areas hide, 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Gaussian softening in px (default 0).' },\n enabled: { type: 'boolean', description: 'false keeps the mask but ignores it.' },\n linked: { type: 'boolean', description: 'false keeps the mask in place when the smart object is transformed or moved (default linked).' },\n },\n required: ['layerId', 'from'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the smart filter mask', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['density', 'feather', 'enabled', 'linked']) if (args[k] !== undefined) props[k] = args[k];\n const mask = controller.setSmartFilterMask(id, args.from as 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props);\n return { layerId: id, filterMask: maskSummary(mask ?? undefined) };\n },\n };\n\n return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];\n}\n","import type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind, ImageAdjustments } from './types';\nimport { normalizeProcessVersion } from './color';\nimport {\n ADJUSTMENT_KINDS,\n isAdjustmentKind,\n} from './adjustment-layers';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport {\n ADJUSTMENT_LAYER_CONTROLS,\n ADJUSTMENT_LAYER_OPACITY_CONTROL,\n IMAGE_ADJUSTMENT_CONTROLS,\n clampControlValue,\n findAdjustmentControl,\n findLayerControl,\n readAdjustmentControls,\n setControlPath,\n} from './controls';\nimport { createImageSelectionTools } from './selection-tools';\nimport { createImageCaptureSurface } from './capture';\nimport { createCaptureTool } from '../tools/capture.js';\nimport { createImageEditTools } from './edit-tools';\nimport { createImageLayerTools } from './layer-tools';\nimport { createImageEffectTools } from './effect-tools';\nimport { createImagePaintTools } from './paint-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\nimport { createImageTransformTools } from './transform-tools';\nimport { createImageSmartObjectTools } from './smart-tools';\n\nexport interface ImageEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `image-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n}\n\nconst LAYER_PATH = /^layers\\.([^.]+)\\.(.+)$/;\n\nfunction adjustmentsSummary(adjustments: ImageAdjustments) {\n return {\n process: normalizeProcessVersion(adjustments.process),\n values: readAdjustmentControls(adjustments),\n layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l))),\n };\n}\n\n/**\n * Agent tools for an image editor, for a `ToolRegistry`:\n *\n * - `get_adjustments`: base slider values (by control id), process version and adjustment layers.\n * - `list_controls`: the control table (ids, ranges, defaults, steps), layer kinds and blend modes.\n * - `set_adjustments {values, label}`: sets base controls (`exposure`) and layer parameters\n * (`layers.<id>.vibrance`, `layers.<id>.opacity`) in one undo step; values are clamped to the\n * control ranges and the applied values are echoed.\n * - `add_adjustment_layer {kind, params?, opacity?, blend?, name?}`.\n * - `render_preview {maxSide}`: the rendered image as a PNG, longest side at most `maxSide`.\n * - `capture_view {region, rect?, ...}`: a screenshot of the whole image, the selection or a rect in\n * natural pixels (up to 8× for close inspection), optionally with the selection shaded.\n * - Selections and masks: `get_selection`, `select_region`, `select_by_color`, `modify_selection`,\n * `refine_selection`, `apply_selection_as_mask` (see `createImageSelectionTools`).\n * - Layers, geometry and history: `get_image_info`, `get_histogram`, `update_adjustment_layer`,\n * `remove_adjustment_layer`, `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`,\n * `flip_image`, `undo_image`, `redo_image` (see `createImageEditTools`).\n * - Free Transform: `get_transform_frame`, `transform_layer`, `warp_layer`, `transform_selection`,\n * `flip_layer`, `rotate_layer_90` (see `createImageTransformTools`).\n * - Smart objects and smart filters: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, `add_smart_filter`, `update_smart_filter`, `remove_smart_filter`,\n * `move_smart_filter`, `set_smart_filters_enabled`, `set_smart_filter_mask` (see\n * `createImageSmartObjectTools`).\n */\nexport function createImageEditorToolProvider(\n controller: ImageEditorController,\n options: ImageEditorToolProviderOptions = {}\n): AgentToolProvider {\n const getAdjustments: AgentTool = {\n name: 'get_adjustments',\n title: 'Read image adjustments',\n description:\n 'Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'Read image adjustments',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => adjustmentsSummary(controller.getState().adjustments),\n };\n\n const listControls: AgentTool = {\n name: 'list_controls',\n title: 'List adjustment controls',\n description:\n 'Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'List adjustment controls',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => ({\n controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({\n id, label, section, min, max, step, default: def, ...(requiresProcess ? { requiresProcess } : {}),\n })),\n layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({\n kind,\n label,\n controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({\n id, label: l, min, max, step, default: def,\n })),\n })),\n blendModes: BLEND_MODES.map((m) => m.id),\n layerPathFormat: 'layers.<layerId>.<controlId>',\n }),\n };\n\n const setAdjustments: AgentTool = {\n name: 'set_adjustments',\n title: 'Set image adjustments',\n description:\n 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. \"exposure\"); layer parameters use \"layers.<layerId>.<controlId>\" (e.g. \"layers.adj_1.vibrance\"). Values are clamped to each control range; the applied values are returned.',\n inputSchema: {\n type: 'object',\n properties: {\n values: {\n type: 'object',\n description: 'Map of control id to number. See list_controls for ids and ranges.',\n additionalProperties: { type: 'number' },\n },\n label: { type: 'string', description: 'Short description of the change (echoed back).' },\n },\n required: ['values'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Set image adjustments',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: ({ values, label }) => {\n const entries = Object.entries((values ?? {}) as Record<string, unknown>);\n if (!entries.length) throw new Error('No values given. Pass {\"values\": {\"<control id>\": number}}.');\n const state = controller.getState();\n const base: Partial<ImageAdjustments> = {};\n const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));\n const applied: Record<string, number> = {};\n const clamped: string[] = [];\n const unknown: string[] = [];\n for (const [id, raw] of entries) {\n const m = LAYER_PATH.exec(id);\n if (m) {\n const layer = layers.find((l) => l.id === m[1]);\n const spec = layer ? findLayerControl(layer.kind, m[2]) : undefined;\n if (!layer || !spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n if (spec.id === 'opacity') layer.opacity = v;\n else layer.params = setControlPath(layer.params, spec.id, v) as typeof layer.params;\n applied[id] = v;\n continue;\n }\n const spec = findAdjustmentControl(id);\n if (!spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n (base as Record<string, number>)[id] = v;\n applied[id] = v;\n }\n if (unknown.length) {\n const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(', ');\n throw new Error(\n `Unknown control id(s): ${unknown.join(', ')}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`\n );\n }\n const changedLayers = Object.keys(applied).some((id) => id.startsWith('layers.'));\n controller.setAdjustments(changedLayers ? { ...base, layers } : base);\n const result: Record<string, unknown> = { applied, clamped };\n if (typeof label === 'string' && label) result.label = label;\n const notes: string[] = [];\n if ('tint' in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {\n notes.push('Tint has no effect on process 1 edits.');\n }\n if (notes.length) result.notes = notes;\n return result;\n },\n };\n\n const addLayer: AgentTool = {\n name: 'add_adjustment_layer',\n title: 'Add an adjustment layer',\n description:\n 'Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: ' +\n ADJUSTMENT_KINDS.map((k) => k.kind).join(', ') +\n '. Omitted params are neutral.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Adjustment kind.' },\n params: { type: 'object', description: 'Initial parameters (see list_controls); merged with neutral defaults.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' },\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations.' },\n parentId: { type: 'string', description: 'Put the layer at the top of this group (from list_layers).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Add an adjustment layer',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {\n if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind \"${kind}\".`);\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n if (aboveId !== undefined && parentId !== undefined) throw new Error('Give aboveId or parentId, not both.');\n for (const ref of [aboveId, parentId]) {\n if (ref !== undefined && !controller.getLayer(ref as string)) throw new Error(`Unknown layer \"${ref}\". Call list_layers for ids.`);\n }\n if (parentId !== undefined && controller.getLayer(parentId as string)?.type !== 'group') throw new Error(`Layer \"${parentId}\" is not a group.`);\n const id = controller.addAdjustmentLayer({ kind: kind as AdjustmentKind, params, opacity, blend, name, aboveId, parentId });\n const layer = controller.getAdjustmentLayer(id)!;\n return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };\n },\n };\n\n const renderPreview: AgentTool = {\n name: 'render_preview',\n title: 'Render a preview',\n description:\n 'Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels. With layerId, renders only that layer (a group with its children) through the same crop and rotation.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerId: { type: 'string', description: 'Render only this layer (from list_layers).' },\n },\n additionalProperties: false,\n },\n annotations: {\n title: 'Render a preview',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: async ({ maxSide, layerId }) => {\n if (layerId !== undefined && !controller.getLayer(layerId as string)) throw new Error(`Unknown layer \"${layerId}\". Call list_layers for ids.`);\n const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === 'number' ? maxSide : 512)));\n const state = controller.getState();\n const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };\n const quarter = Math.abs(Math.round((state.transform.rotate % 180) / 90)) % 2 === 1;\n const w = quarter ? crop.height : crop.width;\n const h = quarter ? crop.width : crop.height;\n const k = Math.min(1, side / Math.max(w, h, 1));\n const dataUrl = await controller.toDataURL({\n format: 'image/png',\n width: Math.max(1, Math.round(w * k)),\n height: Math.max(1, Math.round(h * k)),\n ...(layerId !== undefined ? { layerIds: [layerId as string] } : {}),\n });\n const comma = dataUrl.indexOf(',');\n if (!dataUrl.startsWith('data:image/png') || comma < 0 || comma === dataUrl.length - 1) {\n throw new Error('Rendering is not available in this environment.');\n }\n return {\n content: [\n { type: 'image', data: dataUrl.slice(comma + 1), mimeType: 'image/png' },\n { type: 'text', text: `Preview ${Math.round(w * k)}×${Math.round(h * k)}` },\n ],\n };\n },\n };\n\n return {\n id: options.id ?? 'image-editor',\n namespace: options.namespace,\n tools: [\n getAdjustments,\n listControls,\n setAdjustments,\n addLayer,\n renderPreview,\n createCaptureTool(createImageCaptureSurface(controller), { subject: 'the image (source orientation, before crop and rotation)', defaultPadding: 0 }),\n ...createImageSelectionTools(controller),\n ...createImageEditTools(controller),\n ...createImageLayerTools(controller),\n ...createImageTransformTools(controller),\n ...createImageSmartObjectTools(controller),\n ...createImageEffectTools(controller),\n ...createImagePaintTools(controller),\n ...createPigmentTools(),\n ...createImagePsdTools(controller),\n ],\n };\n}\n\n","import type { CurvePoint } from './types';\nimport { curveLutFromLevels } from './adjustment-layers';\n\n/**\n * Framework-neutral helpers for a Curves editor drawn in a `width` × `height` box (levels 0..255\n * on both axes, output up). The React and Svelte curve editors share them.\n */\n\nconst clampLevel = (v: number) => Math.min(255, Math.max(0, Math.round(v)));\n\n/** Box coordinates of a curve point. */\nexport function curvePointToBox(p: CurvePoint, width: number, height: number): { x: number; y: number } {\n return { x: (p.input / 255) * width, y: height - (p.output / 255) * height };\n}\n\n/** The curve point (whole levels, clamped) under box coordinates. */\nexport function boxToCurvePoint(x: number, y: number, width: number, height: number): CurvePoint {\n return { input: clampLevel((x / (width || 1)) * 255), output: clampLevel(((height - y) / (height || 1)) * 255) };\n}\n\n/** An SVG polyline path of the evaluated curve (spline, clamped, constant beyond the end points). */\nexport function curveSvgPath(points: readonly CurvePoint[], width: number, height: number, samples = 64): string {\n const lut = curveLutFromLevels(points);\n const n = lut.length - 1;\n let d = '';\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n const y = lut[Math.round(t * n)];\n d += `${i === 0 ? 'M' : ' L'}${round2(t * width)} ${round2(height - y * height)}`;\n }\n return d;\n}\n\n/** Index of the point within `radius` box units of (x, y), nearest first; -1 for none. */\nexport function hitTestCurvePoint(\n points: readonly CurvePoint[],\n x: number,\n y: number,\n width: number,\n height: number,\n radius = 8\n): number {\n let best = -1;\n let bestD = radius * radius;\n points.forEach((p, i) => {\n const b = curvePointToBox(p, width, height);\n const d = (b.x - x) ** 2 + (b.y - y) ** 2;\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n });\n return best;\n}\n\n/** Inserts a point in input order (replacing one at the same input). Returns the new list and its index. */\nexport function insertCurvePoint(points: readonly CurvePoint[], p: CurvePoint): { points: CurvePoint[]; index: number } {\n const point = { input: clampLevel(p.input), output: clampLevel(p.output) };\n const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));\n let index = out.findIndex((q) => q.input > point.input);\n if (index === -1) index = out.length;\n out.splice(index, 0, point);\n return { points: out, index };\n}\n\n/** Moves point `index`, keeping its input strictly between its neighbours'. */\nexport function moveCurvePoint(points: readonly CurvePoint[], index: number, p: CurvePoint): CurvePoint[] {\n const out = points.map((q) => ({ ...q }));\n if (index < 0 || index >= out.length) return out;\n const lo = index > 0 ? out[index - 1].input + 1 : 0;\n const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;\n out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };\n return out;\n}\n\n/** Removes point `index`; a curve keeps at least two points. */\nexport function removeCurvePoint(points: readonly CurvePoint[], index: number): CurvePoint[] {\n if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));\n return points.filter((_, i) => i !== index).map((q) => ({ ...q }));\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { ImagePoint } from '../types';\nimport type { ImageEditorController } from '../controller';\nimport { ellipsePoints, normalizeRect } from './shapes';\nimport { selectionModeFromModifiers, type SelectionToolOptions } from './tool-options';\n\nexport interface GestureModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Pointer gestures of the selection tools, shared by the React and Svelte image editors: feed\n * it pointer positions in natural image pixels (`canvasToImagePoint(..., false)`).\n *\n * - Magic wand: selects on `begin` (async, as it may read pixels).\n * - Rectangle / ellipse: drag a box; a click without a drag deselects (replace mode).\n * - Lasso: drag a free-hand path, closed on `end`.\n *\n * Shift adds, Alt subtracts, both intersect (overriding the options bar mode). While dragging,\n * {@link draftPath} is the outline to show (`RenderOptions.draftSelectionPath`).\n */\nexport class SelectionGesture {\n private start: ImagePoint | null = null;\n private points: ImagePoint[] = [];\n private current: ImagePoint | null = null;\n private modifiers: GestureModifiers = {};\n /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */\n public error: string | null = null;\n\n constructor(\n readonly controller: ImageEditorController,\n private readonly getOptions: () => SelectionToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The outline of the shape being drawn, or `null`. */\n get draftPath(): ImagePoint[] | null {\n if (!this.start || !this.current) return null;\n const { tool } = this.getOptions();\n if (tool === 'lasso') return this.points.length > 1 ? this.points.slice() : null;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current.x - this.start.x,\n height: this.current.y - this.start.y,\n });\n if (rect.width < 1 && rect.height < 1) return null;\n if (tool === 'ellipse') return ellipsePoints(rect);\n return [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n { x: rect.x, y: rect.y + rect.height },\n ];\n }\n\n /** Pointer down. Resolves once a wand click has been applied. */\n async begin(point: ImagePoint, modifiers: GestureModifiers = {}): Promise<void> {\n const options = this.getOptions();\n this.error = null;\n this.modifiers = modifiers;\n if (options.tool === 'wand') {\n this.start = null;\n try {\n await this.controller.magicWand(point, {\n tolerance: options.tolerance,\n contiguous: options.contiguous,\n antiAlias: options.antiAlias,\n distance: options.distance,\n sampleMerged: options.sampleMerged,\n sample: options.sample,\n mode: selectionModeFromModifiers(modifiers, options.mode),\n });\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n return;\n }\n this.start = point;\n this.current = point;\n this.points = [point];\n }\n\n /** Pointer move; returns true when the draft changed (re-render). */\n move(point: ImagePoint): boolean {\n if (!this.start) return false;\n this.current = point;\n if (this.getOptions().tool === 'lasso') {\n const last = this.points[this.points.length - 1];\n if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;\n this.points.push(point);\n }\n return true;\n }\n\n /** Pointer up: commits the shape. Returns true when the selection changed. */\n end(point?: ImagePoint, modifiers?: GestureModifiers): boolean {\n if (!this.start) return false;\n if (point) this.move(point);\n const options = this.getOptions();\n const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);\n const path = this.draftPath;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current!.x - this.start.x,\n height: this.current!.y - this.start.y,\n });\n this.start = null;\n this.current = null;\n this.points = [];\n if (!path) return mode === 'replace' ? this.controller.deselect() : false;\n const opts = { mode, antiAlias: options.antiAlias };\n if (options.tool === 'lasso') return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;\n return options.tool === 'ellipse' ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);\n }\n\n /** Abandons a drag without changing the selection. */\n cancel(): void {\n this.start = null;\n this.current = null;\n this.points = [];\n }\n}\n","/**\n * Region-based mosaic (pixelation) and box-blur filters for redacting or anonymizing\n * rectangular bounding boxes on an ImageData-like buffer.\n */\n\nexport interface MosaicRegion {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/**\n\t * When true, coordinates are interpreted as normalized values:\n\t * - [0..1] when all values are <= 1\n\t * - [0..1000] when any value is > 1 (e.g. Gemini 0..1000 bounding boxes)\n\t */\n\tnormalized?: boolean;\n}\n\nexport interface MosaicImageDataLike {\n\tdata: Uint8ClampedArray;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Resolves a MosaicRegion to clamped integer pixel bounds [x0, y0, x1, y1] within the image dimensions.\n */\nexport function resolveMosaicRegionBounds(\n\tregion: MosaicRegion,\n\timgWidth: number,\n\timgHeight: number\n): { x0: number; y0: number; x1: number; y1: number } | null {\n\tif (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {\n\t\treturn null;\n\t}\n\n\tlet rx = region.x;\n\tlet ry = region.y;\n\tlet rw = region.width;\n\tlet rh = region.height;\n\n\tconst isZeroToOne =\n\t\trx >= 0 &&\n\t\try >= 0 &&\n\t\trw > 0 &&\n\t\trh > 0 &&\n\t\trx <= 1 &&\n\t\try <= 1 &&\n\t\trw <= 1 &&\n\t\trh <= 1 &&\n\t\t(region.normalized === true ||\n\t\t\t(region.normalized === undefined &&\n\t\t\t\t(imgWidth > 1 || imgHeight > 1) &&\n\t\t\t\t(!Number.isInteger(rx) || !Number.isInteger(ry) || !Number.isInteger(rw) || !Number.isInteger(rh))));\n\n\tif (isZeroToOne) {\n\t\trx = rx * imgWidth;\n\t\try = ry * imgHeight;\n\t\trw = rw * imgWidth;\n\t\trh = rh * imgHeight;\n\t} else if (region.normalized === true) {\n\t\t// 0..1000 normalized coordinates\n\t\trx = (rx / 1000) * imgWidth;\n\t\try = (ry / 1000) * imgHeight;\n\t\trw = (rw / 1000) * imgWidth;\n\t\trh = (rh / 1000) * imgHeight;\n\t}\n\n\tconst x0 = Math.max(0, Math.min(imgWidth, Math.floor(rx)));\n\tconst y0 = Math.max(0, Math.min(imgHeight, Math.floor(ry)));\n\tconst x1 = Math.max(0, Math.min(imgWidth, Math.ceil(rx + rw)));\n\tconst y1 = Math.max(0, Math.min(imgHeight, Math.ceil(ry + rh)));\n\n\tif (x1 <= x0 || y1 <= y0) return null;\n\treturn { x0, y0, x1, y1 };\n}\n\n/**\n * Pixelates each rectangular region by averaging `blockSize x blockSize` blocks.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionMosaic<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tblockSize: number = 12\n): T {\n\tconst { data, width, height } = imageData;\n\tconst step = Math.max(2, Math.round(blockSize));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\n\t\tfor (let by = y0; by < y1; by += step) {\n\t\t\tconst bh = Math.min(step, y1 - by);\n\t\t\tfor (let bx = x0; bx < x1; bx += step) {\n\t\t\t\tconst bw = Math.min(step, x1 - bx);\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tconst count = bw * bh;\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\trSum += data[idx];\n\t\t\t\t\t\tgSum += data[idx + 1];\n\t\t\t\t\t\tbSum += data[idx + 2];\n\t\t\t\t\t\taSum += data[idx + 3];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst rAvg = Math.round(rSum / count);\n\t\t\t\tconst gAvg = Math.round(gSum / count);\n\t\t\t\tconst bAvg = Math.round(bSum / count);\n\t\t\t\tconst aAvg = Math.round(aSum / count);\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\tdata[idx] = rAvg;\n\t\t\t\t\t\tdata[idx + 1] = gAvg;\n\t\t\t\t\t\tdata[idx + 2] = bAvg;\n\t\t\t\t\t\tdata[idx + 3] = aAvg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n\n/**\n * Applies a separable 2-pass box blur to each rectangular region.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionBlur<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tradius: number = 10\n): T {\n\tconst { data, width, height } = imageData;\n\tconst rad = Math.max(1, Math.min(64, Math.round(radius)));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\t\tconst rw = x1 - x0;\n\t\tconst rh = y1 - y0;\n\t\tconst temp = new Uint8ClampedArray(rw * rh * 4);\n\n\t\t// Horizontal pass into temp\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst srcY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, x - rad);\n\t\t\t\tconst kEnd = Math.min(rw - 1, x + rad);\n\t\t\t\tfor (let kx = kStart; kx <= kEnd; kx++) {\n\t\t\t\t\tconst srcIdx = (srcY * width + (x0 + kx)) * 4;\n\t\t\t\t\trSum += data[srcIdx];\n\t\t\t\t\tgSum += data[srcIdx + 1];\n\t\t\t\t\tbSum += data[srcIdx + 2];\n\t\t\t\t\taSum += data[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (y * rw + x) * 4;\n\t\t\t\ttemp[dstIdx] = Math.round(rSum / count);\n\t\t\t\ttemp[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\ttemp[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\ttemp[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\n\t\t// Vertical pass from temp back into imageData.data\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst dstY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, y - rad);\n\t\t\t\tconst kEnd = Math.min(rh - 1, y + rad);\n\t\t\t\tfor (let ky = kStart; ky <= kEnd; ky++) {\n\t\t\t\t\tconst srcIdx = (ky * rw + x) * 4;\n\t\t\t\t\trSum += temp[srcIdx];\n\t\t\t\t\tgSum += temp[srcIdx + 1];\n\t\t\t\t\tbSum += temp[srcIdx + 2];\n\t\t\t\t\taSum += temp[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (dstY * width + (x0 + x)) * 4;\n\t\t\t\tdata[dstIdx] = Math.round(rSum / count);\n\t\t\t\tdata[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\tdata[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\tdata[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n","/**\n * The layers panel's view model, shared by the React and Svelte panels: the rows to show (top of\n * the stack first, groups indented, collapsed groups hiding their children), where a drop lands,\n * and the menu of layer operations with whether each one applies to the active layer.\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport type { ImageEditorController } from '../controller';\nimport { findLayer, type ImageLayer, type SmartFilter, type SmartObjectLayer } from './model';\nimport type { LayerPlacement } from './ops';\n\nexport interface LayerPanelRow {\n layer: ImageLayer;\n /** Nesting depth (0 = top level). */\n depth: number;\n parentId: string | null;\n /** Index among its siblings, 0 = bottom. */\n index: number;\n active: boolean;\n /** Display name (adjustment layers without a name show their kind). */\n label: string;\n /** Clipped to the layer below (drawn with an indent arrow). */\n clipped: boolean;\n}\n\n/** Rows top to bottom; children of collapsed groups are left out. */\nexport function layerPanelRows(layers: readonly ImageLayer[], activeId: string | null): LayerPanelRow[] {\n const rows: LayerPanelRow[] = [];\n const walk = (list: readonly ImageLayer[], depth: number, parentId: string | null) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const layer = list[i];\n rows.push({\n layer,\n depth,\n parentId,\n index: i,\n active: layer.id === activeId,\n label: layerLabel(layer),\n clipped: Boolean(layer.clipped),\n });\n if (layer.type === 'group' && !layer.collapsed) walk(layer.children, depth + 1, layer.id);\n }\n };\n walk(layers, 0, null);\n return rows;\n}\n\nexport function layerLabel(layer: ImageLayer): string {\n if (layer.name) return layer.name;\n if (layer.type === 'adjustment') return adjustmentKindLabel(layer.kind);\n return layer.type === 'group' ? 'Group' : 'Layer';\n}\n\n/** A short type badge for rows without a thumbnail. */\nexport function layerBadge(layer: ImageLayer): string {\n switch (layer.type) {\n case 'pixel':\n return 'px';\n case 'adjustment':\n return 'adj';\n case 'group':\n return 'grp';\n case 'annotation':\n return layer.annotation.type === 'text' ? 'T' : 'ann';\n case 'smart':\n return 'so';\n }\n}\n\n/** Where a row was dropped on: the top or bottom part of the row, or the middle of a group row. */\nexport type DropZone = 'above' | 'below' | 'inside';\n\n/** The zone for a pointer at `fraction` (0 top .. 1 bottom) of a row's height. */\nexport function dropZone(fraction: number, isGroup: boolean): DropZone {\n if (isGroup && fraction > 0.3 && fraction < 0.7) return 'inside';\n return fraction < 0.5 ? 'above' : 'below';\n}\n\n/** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */\nexport function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null {\n if (dragId === targetId) return null;\n const target = findLayer(layers, targetId);\n if (!target) return null;\n if (zone === 'inside' && target.layer.type === 'group') return { parentId: targetId };\n // The panel lists the top first: dropping above a row means higher in the stack.\n return zone === 'above' ? { aboveId: targetId } : { belowId: targetId };\n}\n\nexport type LayerMenuAction =\n | 'new-layer'\n | 'new-group'\n | 'duplicate'\n | 'delete'\n | 'group-selected'\n | 'ungroup'\n | 'merge-down'\n | 'merge-visible'\n | 'flatten'\n | 'via-copy'\n | 'via-cut'\n | 'add-mask'\n | 'add-mask-selection'\n | 'apply-mask'\n | 'delete-mask'\n | 'toggle-mask'\n | 'toggle-mask-link'\n | 'toggle-clip'\n | 'rasterize'\n | 'convert-smart'\n | 'edit-contents'\n | 'smart-via-copy'\n | 'add-filter-mask';\n\nexport interface LayerMenuItem {\n action: LayerMenuAction;\n label: string;\n enabled: boolean;\n}\n\n/** The layer menu for the controller's current state (active layer, selection). */\nexport function layerMenu(controller: ImageEditorController): LayerMenuItem[] {\n const s = controller.getState();\n const active = controller.getActiveLayer();\n const loc = active ? findLayer(s.layers, active.id) : null;\n const siblings = loc ? (loc.parent ? loc.parent.children : s.layers) : [];\n const below = loc ? siblings[loc.index - 1] : undefined;\n const hasSel = controller.hasSelection;\n const isPixel = active?.type === 'pixel';\n return [\n { action: 'new-layer', label: 'New Layer', enabled: true },\n { action: 'new-group', label: 'New Group', enabled: true },\n { action: 'duplicate', label: 'Duplicate Layer', enabled: Boolean(active) },\n { action: 'delete', label: 'Delete Layer', enabled: Boolean(active) },\n { action: 'group-selected', label: 'Group Layer', enabled: Boolean(active) },\n { action: 'ungroup', label: 'Ungroup', enabled: active?.type === 'group' },\n { action: 'merge-down', label: 'Merge Down', enabled: Boolean(active?.visible && below?.type === 'pixel') },\n { action: 'merge-visible', label: 'Merge Visible', enabled: s.layers.some((l) => l.visible) },\n { action: 'flatten', label: 'Flatten Image', enabled: s.layers.length > 0 },\n { action: 'via-copy', label: 'Layer via Copy', enabled: isPixel && hasSel },\n { action: 'via-cut', label: 'Layer via Cut', enabled: isPixel && hasSel },\n { action: 'add-mask', label: 'Add Layer Mask', enabled: Boolean(active) },\n { action: 'add-mask-selection', label: 'Add Mask from Selection', enabled: Boolean(active) && hasSel },\n { action: 'apply-mask', label: 'Apply Layer Mask', enabled: isPixel && Boolean(active?.mask) },\n { action: 'delete-mask', label: 'Delete Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask', label: active?.mask?.enabled === false ? 'Enable Layer Mask' : 'Disable Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask-link', label: active?.mask?.linked === false ? 'Link Layer Mask' : 'Unlink Layer Mask', enabled: Boolean(active?.mask && !active.mask.source) },\n { action: 'rasterize', label: 'Rasterize Layer', enabled: active?.type === 'annotation' || active?.type === 'smart' || (isPixel && Boolean(active?.type === 'pixel' && active.filters?.length)) },\n { action: 'convert-smart', label: 'Convert to Smart Object', enabled: Boolean(active && active.type !== 'annotation') },\n { action: 'edit-contents', label: 'Edit Contents', enabled: active?.type === 'smart' },\n { action: 'smart-via-copy', label: 'New Smart Object via Copy', enabled: active?.type === 'smart' },\n { action: 'add-filter-mask', label: 'Add Smart Filter Mask', enabled: active?.type === 'smart' && !active.smartFilters?.mask },\n { action: 'toggle-clip', label: active?.clipped ? 'Release Clipping Mask' : 'Create Clipping Mask', enabled: Boolean(loc && loc.index > 0) },\n ];\n}\n\n/** Runs a menu action on the active layer. Resolves to the error message when it was refused. */\nexport async function runLayerMenuAction(controller: ImageEditorController, action: LayerMenuAction): Promise<string | null> {\n const active = controller.getActiveLayer();\n const id = active?.id;\n try {\n switch (action) {\n case 'new-layer':\n controller.addPixelLayer();\n break;\n case 'new-group':\n controller.addGroup();\n break;\n case 'duplicate':\n if (id) controller.duplicateLayer(id);\n break;\n case 'delete':\n if (id) controller.removeLayer(id);\n break;\n case 'group-selected':\n if (id) controller.groupLayers([id]);\n break;\n case 'ungroup':\n if (id) controller.ungroupLayer(id);\n break;\n case 'merge-down':\n if (id) await controller.mergeDown(id);\n break;\n case 'merge-visible':\n await controller.mergeVisible();\n break;\n case 'flatten':\n await controller.flattenImage();\n break;\n case 'via-copy':\n case 'via-cut':\n controller.layerViaCopy({ cut: action === 'via-cut' });\n break;\n case 'add-mask':\n if (id) controller.addLayerMask(id, 'reveal-all');\n break;\n case 'add-mask-selection':\n if (id) controller.addLayerMask(id, 'selection');\n break;\n case 'apply-mask':\n if (id) await controller.applyLayerMask(id);\n break;\n case 'delete-mask':\n if (id) controller.removeLayerMask(id);\n break;\n case 'toggle-mask':\n if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });\n break;\n case 'toggle-mask-link':\n if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });\n break;\n case 'rasterize':\n if (id) await controller.rasterizeLayer(id);\n break;\n case 'convert-smart':\n if (id) controller.convertToSmartObject([id]);\n break;\n case 'edit-contents':\n if (id) controller.editSmartObjectContents(id);\n break;\n case 'smart-via-copy':\n if (id) controller.copySmartObject(id, false);\n break;\n case 'add-filter-mask':\n if (id) controller.setSmartFilterMask(id, 'reveal-all');\n break;\n case 'toggle-clip':\n if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });\n break;\n }\n return null;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n}\n\n/** One row of a smart object's filter list (Photoshop shows them under the layer, top filter first). */\nexport interface SmartFilterRow {\n filter: SmartFilter;\n /** Display name: Develop, Blur (radius), or the Photoshop filter's name. */\n label: string;\n /** Index in the stack, 0 = bottom (applied first). */\n index: number;\n /** False for Photoshop filters we keep but do not draw. */\n drawn: boolean;\n}\n\n/** The filter rows of a smart object, top filter first (empty when it has none). */\nexport function smartFilterRows(layer: ImageLayer): SmartFilterRow[] {\n if (layer.type !== 'smart') return [];\n const list = (layer as SmartObjectLayer).smartFilters?.filters ?? [];\n const rows: SmartFilterRow[] = list.map((f, index) => ({\n filter: f,\n index,\n drawn: f.kind !== 'psd',\n label: f.kind === 'psd' ? f.name.replace(/&/g, '').replace(/\\.\\.\\.$/, '') : f.kind === 'blur' ? `Blur (${f.radius} px)` : 'Develop',\n }));\n return rows.reverse();\n}\n","/**\n * The layer effects section's view model, shared by the React and Svelte panels: whether a layer\n * can take effects, its effect rows, the menu of kinds to add, and per effect a flat list of\n * controls (sliders, colours, toggles, choices) with how a change maps to `updateLayerEffect`.\n */\n\nimport { BLEND_MODES } from '../blend';\nimport {\n defaultEffectGradient,\n hasActiveEffects,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type EffectFill,\n type LayerEffect,\n type LayerEffectKind,\n} from '../../raster/effects/model';\nimport { builtinPattern, builtinPatternIds, type EffectPattern } from '../../raster/effects/patterns';\nimport type { ImageLayer } from './model';\n\nexport interface LayerEffectRow {\n id: string;\n kind: LayerEffectKind;\n label: string;\n enabled: boolean;\n}\n\nexport interface LayerEffectsPanelModel {\n /** Whether the layer can carry effects (adjustment layers cannot; a fully locked layer refuses changes). */\n available: boolean;\n /** Why not, when unavailable or read-only. */\n reason?: string;\n readOnly: boolean;\n enabled: boolean;\n /** Scale Effects, 1 = 100 %. */\n scale: number;\n rows: LayerEffectRow[];\n}\n\n/** The effects section for `layer`. */\nexport function layerEffectsPanel(layer: ImageLayer | null | undefined): LayerEffectsPanelModel {\n const empty = { readOnly: true, enabled: true, scale: 1, rows: [] };\n if (!layer) return { ...empty, available: false, reason: 'Select a layer to add effects.' };\n if (layer.type === 'adjustment') return { ...empty, available: false, reason: 'Adjustment layers cannot have effects.' };\n const fx = layer.effects;\n const readOnly = Boolean(layer.locks?.all);\n return {\n available: true,\n readOnly,\n reason: readOnly ? 'The layer is locked; unlock it to change its effects.' : undefined,\n enabled: fx?.enabled ?? true,\n scale: fx?.scale ?? 1,\n rows: (fx?.items ?? []).map((e) => ({ id: e.id, kind: e.kind, label: layerEffectLabel(e.kind), enabled: e.enabled })),\n };\n}\n\n/** The fx badge of a layer row: `on` (effects render), `off` (effects but switched off), or `null`. */\nexport function layerEffectsBadge(layer: ImageLayer): 'on' | 'off' | null {\n if (!layer.effects?.items.length) return null;\n return hasActiveEffects(layer.effects) ? 'on' : 'off';\n}\n\n/** The kinds the \"Add effect\" menu offers, in the Layer Style dialog's order. */\nexport const LAYER_EFFECT_MENU: ReadonlyArray<{ kind: LayerEffectKind; label: string }> = LAYER_EFFECT_KINDS.map((kind) => ({ kind, label: layerEffectLabel(kind) }));\n\nexport type EffectControl =\n | { key: string; label: string; type: 'range'; min: number; max: number; step: number; value: number; /** Shown and edited as percent (value × 100). */ percent?: boolean; unit?: string }\n | { key: string; label: string; type: 'color'; value: string }\n | { key: string; label: string; type: 'checkbox'; value: boolean }\n | { key: string; label: string; type: 'select'; value: string; options: Array<{ value: string; label: string }> };\n\nconst BLENDS = BLEND_MODES.filter((m) => m.id !== 'pigment').map((m) => ({ value: m.id as string, label: m.label }));\nconst opt = (...pairs: Array<[string, string]>) => pairs.map(([value, label]) => ({ value, label }));\n\nfunction patternOptions(patterns: readonly EffectPattern[]): Array<{ value: string; label: string }> {\n return [...patterns.map((p) => ({ value: p.id, label: p.name || p.id })), ...builtinPatternIds().map((id) => ({ value: id, label: builtinPattern(id)!.name }))];\n}\n\ntype Rec = Record<string, unknown>;\n\n/** Reads a dotted path (`fill.color`, `gradient.stops.last.color`). */\nfunction getPath(o: unknown, path: string): unknown {\n let v: unknown = o;\n for (const p of path.split('.')) {\n if (v === null || v === undefined) return undefined;\n if (Array.isArray(v)) v = p === 'last' ? v[v.length - 1] : v[Number(p)];\n else v = (v as Rec)[p];\n }\n return v;\n}\n\n/** A copy of `o` with `path` set to `value` (arrays and objects copied along the path). */\nfunction setPath(o: unknown, parts: string[], value: unknown): unknown {\n if (!parts.length) return value;\n const [p, ...rest] = parts;\n if (Array.isArray(o)) {\n const i = p === 'last' ? o.length - 1 : Number(p);\n const out = [...o];\n out[i] = setPath(o[i], rest, value);\n return out;\n }\n const r = (o ?? {}) as Rec;\n return { ...r, [p]: setPath(r[p], rest, value) };\n}\n\nfunction fillControls(f: EffectFill, allowPattern: boolean, patterns: readonly EffectPattern[]): EffectControl[] {\n const types = allowPattern ? opt(['color', 'Color'], ['gradient', 'Gradient'], ['pattern', 'Pattern']) : opt(['color', 'Color'], ['gradient', 'Gradient']);\n const out: EffectControl[] = [{ key: 'fill.type', label: 'Fill', type: 'select', value: f.type, options: types }];\n if (f.type === 'color') out.push({ key: 'fill.color', label: 'Color', type: 'color', value: f.color });\n else if (f.type === 'gradient') out.push(...gradientControls('fill.gradient', f.gradient));\n else {\n out.push(\n { key: 'fill.patternId', label: 'Pattern', type: 'select', value: f.patternId, options: patternOptions(patterns) },\n { key: 'fill.scale', label: 'Scale', type: 'range', min: 0.01, max: 10, step: 0.01, value: f.scale, percent: true, unit: '%' }\n );\n }\n return out;\n}\n\nfunction gradientControls(prefix: string, g: { stops: Array<{ color: string }>; style: string; angle: number; scale: number; reverse: boolean; align: boolean }): EffectControl[] {\n return [\n { key: `${prefix}.stops.0.color`, label: 'Start colour', type: 'color', value: g.stops[0]?.color ?? '#000000' },\n { key: `${prefix}.stops.last.color`, label: 'End colour', type: 'color', value: g.stops[g.stops.length - 1]?.color ?? '#ffffff' },\n { key: `${prefix}.style`, label: 'Style', type: 'select', value: g.style, options: opt(['linear', 'Linear'], ['radial', 'Radial'], ['angle', 'Angle'], ['reflected', 'Reflected'], ['diamond', 'Diamond']) },\n { key: `${prefix}.angle`, label: 'Angle', type: 'range', min: -180, max: 180, step: 1, value: g.angle, unit: '°' },\n { key: `${prefix}.scale`, label: 'Scale', type: 'range', min: 0.1, max: 1.5, step: 0.01, value: g.scale, percent: true, unit: '%' },\n { key: `${prefix}.reverse`, label: 'Reverse', type: 'checkbox', value: g.reverse },\n { key: `${prefix}.align`, label: 'Align with layer', type: 'checkbox', value: g.align },\n ];\n}\n\nconst px = (key: string, label: string, value: number, max = 250): EffectControl => ({ key, label, type: 'range', min: 0, max, step: 1, value, unit: 'px' });\nconst pct = (key: string, label: string, value: number, min = 0, max = 1): EffectControl => ({ key, label, type: 'range', min, max, step: 0.01, value, percent: true, unit: '%' });\nconst deg = (key: string, label: string, value: number, min = -180, max = 180): EffectControl => ({ key, label, type: 'range', min, max, step: 1, value, unit: '°' });\n\n/**\n * The controls for one effect, in the Layer Style dialog's order. `patterns` are the document's\n * patterns (the built-in ones are always offered).\n */\nexport function layerEffectControls(e: LayerEffect, patterns: readonly EffectPattern[] = []): EffectControl[] {\n const blended: EffectControl[] =\n e.kind === 'bevel-emboss'\n ? []\n : [\n { key: 'blend', label: 'Blend mode', type: 'select', value: e.blend, options: BLENDS },\n pct('opacity', 'Opacity', e.opacity),\n ];\n switch (e.kind) {\n case 'drop-shadow':\n case 'inner-shadow':\n return [\n ...blended,\n { key: 'color', label: 'Color', type: 'color', value: e.color },\n { key: 'useGlobalLight', label: 'Use global light', type: 'checkbox', value: e.useGlobalLight },\n deg('angle', 'Angle', e.angle),\n px('distance', 'Distance', e.distance, 500),\n e.kind === 'drop-shadow' ? pct('spread', 'Spread', e.spread) : pct('choke', 'Choke', e.choke),\n px('size', 'Size', e.size),\n pct('noise', 'Noise', e.noise),\n ...(e.kind === 'drop-shadow' ? [{ key: 'knocksOut', label: 'Layer knocks out shadow', type: 'checkbox', value: e.knocksOut } as EffectControl] : []),\n ];\n case 'outer-glow':\n case 'inner-glow':\n return [\n ...blended,\n ...fillControls(e.fill, false, patterns),\n { key: 'technique', label: 'Technique', type: 'select', value: e.technique, options: opt(['softer', 'Softer'], ['precise', 'Precise']) },\n ...(e.kind === 'inner-glow' ? [{ key: 'source', label: 'Source', type: 'select', value: e.source, options: opt(['edge', 'Edge'], ['center', 'Center']) } as EffectControl] : []),\n e.kind === 'outer-glow' ? pct('spread', 'Spread', e.spread) : pct('choke', 'Choke', e.choke),\n px('size', 'Size', e.size),\n pct('range', 'Range', e.range, 0.01, 1),\n pct('noise', 'Noise', e.noise),\n ];\n case 'bevel-emboss':\n return [\n { key: 'style', label: 'Style', type: 'select', value: e.style, options: opt(['inner-bevel', 'Inner Bevel'], ['outer-bevel', 'Outer Bevel'], ['emboss', 'Emboss'], ['pillow-emboss', 'Pillow Emboss'], ['stroke-emboss', 'Stroke Emboss']) },\n { key: 'technique', label: 'Technique', type: 'select', value: e.technique, options: opt(['smooth', 'Smooth'], ['chisel-hard', 'Chisel Hard'], ['chisel-soft', 'Chisel Soft']) },\n pct('depth', 'Depth', e.depth, 0, 10),\n { key: 'direction', label: 'Direction', type: 'select', value: e.direction, options: opt(['up', 'Up'], ['down', 'Down']) },\n px('size', 'Size', e.size),\n px('soften', 'Soften', e.soften, 16),\n { key: 'useGlobalLight', label: 'Use global light', type: 'checkbox', value: e.useGlobalLight },\n deg('angle', 'Angle', e.angle),\n deg('altitude', 'Altitude', e.altitude, 0, 90),\n { key: 'highlightColor', label: 'Highlight', type: 'color', value: e.highlightColor },\n { key: 'highlightBlend', label: 'Highlight mode', type: 'select', value: e.highlightBlend, options: BLENDS },\n pct('highlightOpacity', 'Highlight opacity', e.highlightOpacity),\n { key: 'shadowColor', label: 'Shadow', type: 'color', value: e.shadowColor },\n { key: 'shadowBlend', label: 'Shadow mode', type: 'select', value: e.shadowBlend, options: BLENDS },\n pct('shadowOpacity', 'Shadow opacity', e.shadowOpacity),\n ];\n case 'satin':\n return [\n ...blended,\n { key: 'color', label: 'Color', type: 'color', value: e.color },\n deg('angle', 'Angle', e.angle),\n px('distance', 'Distance', e.distance),\n px('size', 'Size', e.size),\n { key: 'invert', label: 'Invert', type: 'checkbox', value: e.invert },\n ];\n case 'color-overlay':\n return [...blended, { key: 'color', label: 'Color', type: 'color', value: e.color }];\n case 'gradient-overlay':\n return [...blended, ...gradientControls('gradient', e.gradient)];\n case 'pattern-overlay':\n return [\n ...blended,\n { key: 'patternId', label: 'Pattern', type: 'select', value: e.patternId, options: patternOptions(patterns) },\n pct('scale', 'Scale', e.scale, 0.01, 10),\n deg('angle', 'Angle', e.angle ?? 0),\n { key: 'link', label: 'Link with layer', type: 'checkbox', value: e.link ?? true },\n ];\n case 'stroke':\n return [\n ...blended,\n px('size', 'Size', e.size),\n { key: 'position', label: 'Position', type: 'select', value: e.position, options: opt(['outside', 'Outside'], ['inside', 'Inside'], ['center', 'Center']) },\n ...fillControls(e.fill, true, patterns),\n ];\n }\n}\n\n/** A new fill of `type`, keeping the colour where it can. */\nfunction fillOfType(current: EffectFill, type: string, patterns: readonly EffectPattern[]): EffectFill {\n if (type === 'gradient') return { type: 'gradient', gradient: defaultEffectGradient() };\n if (type === 'pattern') return { type: 'pattern', patternId: patterns[0]?.id ?? 'checker', scale: 1 };\n return { type: 'color', color: current.type === 'color' ? current.color : '#000000' };\n}\n\n/**\n * The `updateLayerEffect` settings for setting control `key` of `effect` to `value` (a nested\n * object such as `fill` or `gradient` is replaced whole, with the one field changed). Values are\n * in the effect's own units (fractions for percent controls).\n */\nexport function effectControlChange(effect: LayerEffect, key: string, value: unknown, patterns: readonly EffectPattern[] = []): Record<string, unknown> {\n const [top, ...rest] = key.split('.');\n if (top === 'fill' && rest[0] === 'type') return { fill: fillOfType((effect as { fill: EffectFill }).fill, String(value), patterns) };\n if (top === 'patternId' && !rest.length) {\n const name = patternOptions(patterns).find((o) => o.value === value)?.label;\n return name ? { patternId: value, name } : { patternId: value };\n }\n if (!rest.length) return { [top]: value };\n // A pattern name follows its id.\n if (top === 'fill' && rest[0] === 'patternId') {\n const name = patternOptions(patterns).find((o) => o.value === value)?.label;\n return { fill: { ...(effect as { fill: Rec }).fill, patternId: value, ...(name ? { name } : {}) } };\n }\n return { [top]: setPath(getPath(effect, top), rest, value) };\n}\n","/** Framework-free helpers behind the PSD buttons of the React and Svelte layer panels. */\n\nimport type { ImageEditorController } from '../controller';\n\n/** Types the file pickers accept. */\nexport const PSD_FILE_ACCEPT = '.psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop';\n\nfunction blobBytes(file: Blob): Promise<Uint8Array> {\n if (typeof file.arrayBuffer === 'function') return file.arrayBuffer().then((b) => new Uint8Array(b));\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));\n reader.onerror = () => reject(reader.error ?? new Error('The file could not be read.'));\n reader.readAsArrayBuffer(file);\n });\n}\n\n/** Reads a picked file and opens it as the document (one undo step). Returns the import warnings. */\nexport async function openPsdFile(controller: ImageEditorController, file: Blob): Promise<string[]> {\n return controller.loadPsd(await blobBytes(file)).warnings;\n}\n\n/** A file name for the exported document: `name` with a `.psd` / `.psb` extension. */\nexport function psdFileName(name: string, psb: boolean): string {\n const base = name.replace(/\\.[a-z0-9]{2,4}$/i, '') || 'image';\n return `${base}.${psb ? 'psb' : 'psd'}`;\n}\n\n/**\n * Writes the document as a PSD and offers it as a download (in a browser). Returns the file\n * name and the export warnings.\n */\nexport async function downloadPsd(controller: ImageEditorController, name = 'image'): Promise<{ fileName: string; warnings: string[] }> {\n const res = await controller.toPsd();\n const fileName = psdFileName(name, res.psb);\n if (typeof document !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([res.bytes as BlobPart], { type: 'image/vnd.adobe.photoshop' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n }\n return { fileName, warnings: res.warnings };\n}\n","/**\n * Free Transform's view model, shared by the React and Svelte editors: the overlay (outline,\n * eight handles, the reference point), what a pointer is over, Photoshop's drags and modifiers,\n * the options bar's readout and fields, and the keyboard (Enter commits, Esc cancels, arrows\n * nudge). It drives a `FreeTransformSession`; the bindings forward pointer and key events and draw\n * `overlay()` through the renderer's `freeTransform` option.\n *\n * Drags (as Photoshop CC, where a corner scales proportionally by default):\n * - corner: scale from the opposite corner (Alt: from the reference point); Shift toggles\n * proportional; side: scale one axis;\n * - Mod (Ctrl / Cmd) + corner: distort that corner; Mod + side: skew along the side (Alt: about\n * the centre); Mod + Alt + Shift + corner: perspective;\n * - outside the box: rotate about the reference point (Shift: 15° steps); inside: move (Shift:\n * along one axis); the reference point: move it;\n * - the mode switch (rotate, skew, distort, perspective) makes every handle do that;\n * - Warp: drag a mesh anchor or handle (the grid stays smooth across its lines), or anywhere\n * inside to pull the surface; presets and the grid size come from the options bar. The warp is\n * applied in the frame's own space before the corner map, so both can be used together.\n */\n\nimport type { ImageEditorController } from '../controller';\nimport type { FreeTransformInit, FreeTransformSession, FreeTransformResult } from '../free-transform';\nimport {\n cornersFromMatrix,\n describeTransform,\n matrixFromParams,\n type FreeTransformMode,\n type Quad,\n type TransformAnchor,\n type TransformReadout,\n} from '../transform';\nimport { applyHomography, invertHomography, rectToQuad, type Homography } from '../../geom/homography';\nimport {\n WARP_STYLES,\n WARP_STYLE_LABELS,\n dragWarpMeshPoint,\n identityWarpMesh,\n pullWarpMesh,\n resizeWarpMesh,\n warpMeshEditPoints,\n warpMeshGridCurves,\n warpMeshParamAt,\n warpPresetMesh,\n type WarpPreset,\n} from '../../geom/warp';\nimport type { WarpMesh, WarpStyle } from '../../geom/types';\nimport type { Vec2 } from '../../geom/types';\nimport type { Interpolation } from '../../raster/transform';\nimport type { ImagePoint } from '../types';\nimport { flattenLayers } from './model';\nimport type { ShortcutEvent } from './paint-ui';\n\n/** What a pointer is over (`mesh:<index>`: a warp mesh control point). */\nexport type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;\n\nconst CORNERS = ['nw', 'ne', 'se', 'sw'] as const;\nconst SIDES = ['n', 'e', 's', 'w'] as const;\nconst CORNER_INDEX: Record<string, number> = { nw: 0, ne: 1, se: 2, sw: 3 };\nconst SIDE_CORNERS: Record<string, [number, number]> = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };\n/** Box fractions of each handle. */\nconst HANDLE_FRACTION: Record<string, [number, number]> = { nw: [0, 0], n: [0.5, 0], ne: [1, 0], e: [1, 0.5], se: [1, 1], s: [0.5, 1], sw: [0, 1], w: [0, 0.5] };\nconst ANCHOR_FRACTION: Record<TransformAnchor, [number, number]> = {\n 'top-left': [0, 0],\n top: [0.5, 0],\n 'top-right': [1, 0],\n left: [0, 0.5],\n center: [0.5, 0.5],\n right: [1, 0.5],\n 'bottom-left': [0, 1],\n bottom: [0.5, 1],\n 'bottom-right': [1, 1],\n};\n\nexport interface TransformModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n /** Ctrl or Cmd. */\n ctrlKey?: boolean;\n metaKey?: boolean;\n}\n\n/** The modes of the options bar's switch (Warp comes with its own mesh UI). */\nexport const FREE_TRANSFORM_MODE_OPTIONS: readonly { id: FreeTransformMode; label: string; title: string }[] = [\n { id: 'scale', label: 'Free', title: 'Free Transform: corners scale, outside rotates, Ctrl/Cmd distorts' },\n { id: 'rotate', label: 'Rotate', title: 'Every handle rotates about the reference point' },\n { id: 'skew', label: 'Skew', title: 'Sides slide along themselves' },\n { id: 'distort', label: 'Distort', title: 'Each corner moves freely' },\n { id: 'perspective', label: 'Perspective', title: 'A corner and its partner move symmetrically' },\n { id: 'warp', label: 'Warp', title: 'Bend through a mesh: drag its points or pull inside; presets in the bar' },\n];\n\n/** The Warp style menu: Custom (the mesh as dragged) and Photoshop's fifteen presets. */\nexport const WARP_STYLE_OPTIONS: readonly { id: WarpStyle | 'custom'; label: string }[] = [\n { id: 'custom', label: 'Custom' },\n ...WARP_STYLES.map((id) => ({ id, label: WARP_STYLE_LABELS[id] })),\n];\n\n/** The Warp grid menu: patches across and down. */\nexport const WARP_GRID_OPTIONS: readonly { id: string; label: string; columns: number; rows: number }[] = [\n { id: '1x1', label: 'Default', columns: 1, rows: 1 },\n { id: '3x3', label: '3 × 3', columns: 3, rows: 3 },\n { id: '5x5', label: '5 × 5', columns: 5, rows: 5 },\n];\n\n/** Warp preset settings as the options bar shows them (percent). */\nexport interface WarpBar {\n style: WarpStyle | 'custom';\n bend: number;\n horizontal: number;\n vertical: number;\n orientation: 'horizontal' | 'vertical';\n columns: number;\n rows: number;\n}\n\n/** The options bar's numeric fields (Photoshop's X, Y, W, H, angle and skews). */\nexport const FREE_TRANSFORM_FIELDS = [\n { id: 'x', label: 'X', unit: 'px', title: 'Reference point X (document pixels)' },\n { id: 'y', label: 'Y', unit: 'px', title: 'Reference point Y (document pixels)' },\n { id: 'scaleX', label: 'W', unit: '%', title: 'Width in percent of the original' },\n { id: 'scaleY', label: 'H', unit: '%', title: 'Height in percent of the original' },\n { id: 'rotate', label: '∠', unit: '°', title: 'Rotation in degrees, clockwise' },\n { id: 'skewX', label: 'H skew', unit: '°', title: 'Horizontal skew in degrees' },\n { id: 'skewY', label: 'V skew', unit: '°', title: 'Vertical skew in degrees' },\n] as const;\n\nexport type FreeTransformField = (typeof FREE_TRANSFORM_FIELDS)[number]['id'];\n\n/** What the options bar shows. */\nexport interface FreeTransformBar extends Record<FreeTransformField, number> {\n /** False after Distort or Perspective: the numbers no longer describe the shape. */\n affine: boolean;\n}\n\nexport interface TransformOverlay {\n /** The transformed frame, corner to corner (document pixels). */\n outline: ImagePoint[];\n /** The eight handles (none in Warp mode). */\n handles: Array<{ id: 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'; x: number; y: number }>;\n reference: ImagePoint;\n /** In Warp mode: the mesh's grid lines as cubics, its anchors, and its handles with the anchor each belongs to. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n}\n\nconst CURSOR: Record<TransformHandle, string> = {\n nw: 'nwse-resize',\n se: 'nwse-resize',\n ne: 'nesw-resize',\n sw: 'nesw-resize',\n n: 'ns-resize',\n s: 'ns-resize',\n e: 'ew-resize',\n w: 'ew-resize',\n reference: 'move',\n inside: 'move',\n outside: 'alias',\n};\nconst meshIndex = (h: TransformHandle | null) => (h && h.startsWith('mesh:') ? Number(h.slice(5)) : -1);\n\nconst mod = (m: TransformModifiers) => Boolean(m.ctrlKey || m.metaKey);\nconst add = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y });\nconst sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });\n\nfunction rotateAbout(p: Vec2, c: Vec2, rad: number): Vec2 {\n const cos = Math.cos(rad);\n const sin = Math.sin(rad);\n const d = sub(p, c);\n return { x: c.x + d.x * cos - d.y * sin, y: c.y + d.x * sin + d.y * cos };\n}\n\nfunction insideQuad(q: readonly Vec2[], p: Vec2): boolean {\n let sign = 0;\n for (let i = 0; i < 4; i++) {\n const a = q[i];\n const b = q[(i + 1) % 4];\n const c = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);\n if (Math.abs(c) < 1e-12) continue;\n const s = Math.sign(c);\n if (sign && s !== sign) return false;\n sign = s;\n }\n return true;\n}\n\n/** Even-odd point-in-polygon (the warped mesh's border). */\nfunction insidePolygon(poly: readonly Vec2[], p: Vec2): boolean {\n let inside = false;\n for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n const a = poly[i];\n const b = poly[j];\n if (a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;\n }\n return inside;\n}\n\nexport interface FreeTransformToolOptions {\n /** Defers preview resampling to the next frame (pass `requestAnimationFrame`; default: at once). */\n schedule?: (run: () => void) => void;\n /** Called whenever the overlay, readout or error changes. */\n onChange?: () => void;\n}\n\n/**\n * The interactive Free Transform (see the module comment). `begin` opens a session on the\n * controller; pointer and key methods drive it; `commit` / `cancel` close it.\n */\nexport class FreeTransformTool {\n private sessionRef: FreeTransformSession | null = null;\n private modeValue: FreeTransformMode = 'scale';\n /** The reference point in the untransformed frame's space (follows the transform). */\n private refBox: Vec2 = { x: 0, y: 0 };\n private corners: Quad | null = null;\n private drag: { handle: TransformHandle; start: Vec2; corners: Quad; refBox: Vec2; ref: Vec2; mesh: WarpMesh | null; st: { s: number; t: number } | null } | null = null;\n /** The warp mesh in the frame's own space (null: no warp). */\n private mesh: WarpMesh | null = null;\n private warpSettings: WarpBar = { style: 'custom', bend: 50, horizontal: 0, vertical: 0, orientation: 'horizontal', columns: 1, rows: 1 };\n private pending = false;\n private viewScale = 1;\n private viewClip: { x: number; y: number; width: number; height: number } | undefined;\n /** Values typed into the options bar since the last drag (V skew is not recovered from the shape). */\n private typed: Partial<Record<FreeTransformField, number>> = {};\n /** Width and height scale together when typed (the chain icon). */\n proportional = true;\n error: string | null = null;\n lastResult: FreeTransformResult | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: FreeTransformToolOptions = {}\n ) {}\n\n get active(): boolean {\n return Boolean(this.sessionRef?.isOpen);\n }\n\n get session(): FreeTransformSession | null {\n return this.active ? this.sessionRef : null;\n }\n\n get mode(): FreeTransformMode {\n return this.modeValue;\n }\n\n get interpolation(): Interpolation {\n return this.sessionRef?.interpolation ?? 'bicubic';\n }\n\n private readonly listeners = new Set<() => void>();\n\n /** Calls `listener` whenever the overlay, readout, mode or error changes. Returns the unsubscribe. */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private changed(): void {\n this.options.onChange?.();\n for (const l of this.listeners) l();\n }\n\n /** Opens a Free Transform (default: the selected pixels of the active layer, or the whole layer). Returns an error message or `null`. */\n begin(init: FreeTransformInit = {}): string | null {\n if (this.active) return null;\n try {\n if (!init.layerId && init.content !== 'selection-outline' && !this.controller.getState().activeLayerId) {\n // Photoshop always has a target layer: use the topmost visible pixel layer.\n const top = [...flattenLayers(this.controller.getState().layers)].reverse().find((l) => l.type === 'pixel' && l.visible);\n if (top) this.controller.setActiveLayer(top.id);\n }\n const s = this.controller.beginFreeTransform(init);\n this.sessionRef = s;\n this.corners = s.geometry.corners;\n this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };\n this.modeValue = 'scale';\n // A smart object's warp is part of its placement: keep editing it.\n this.mesh = s.geometry.warp ?? null;\n this.warpSettings = { style: 'custom', bend: 50, horizontal: 0, vertical: 0, orientation: 'horizontal', columns: 1, rows: 1 };\n this.typed = {};\n this.error = null;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n }\n this.changed();\n return this.error;\n }\n\n setMode(mode: FreeTransformMode): void {\n this.modeValue = mode;\n const s = this.session;\n if (mode === 'warp' && s && !this.mesh) this.mesh = identityWarpMesh(s.box, this.warpSettings.columns, this.warpSettings.rows);\n this.changed();\n }\n\n /** The Warp options bar's settings. */\n warpBar(): WarpBar {\n return { ...this.warpSettings };\n }\n\n /** The warp mesh (frame space), or null. */\n get warp(): WarpMesh | null {\n return this.mesh;\n }\n\n /**\n * Applies Warp options: a preset `style` with `bend` / `horizontal` / `vertical` (percent) and\n * `orientation` replaces the mesh; `custom` keeps it; the grid size re-splits it (exactly when it\n * can). Switches to Warp mode.\n */\n setWarp(changes: Partial<WarpBar>): void {\n const s = this.session;\n if (!s) return;\n this.modeValue = 'warp';\n const next = { ...this.warpSettings, ...changes };\n this.warpSettings = next;\n let mesh = this.mesh ?? identityWarpMesh(s.box, next.columns, next.rows);\n if (next.style !== 'custom' && ('style' in changes || 'bend' in changes || 'horizontal' in changes || 'vertical' in changes || 'orientation' in changes)) {\n const preset: WarpPreset = { style: next.style, bend: next.bend, horizontal: next.horizontal, vertical: next.vertical, orientation: next.orientation };\n mesh = warpPresetMesh(preset, s.box) ?? identityWarpMesh(s.box);\n mesh = resizeWarpMesh(mesh, next.columns, next.rows);\n }\n if ('columns' in changes || 'rows' in changes) mesh = resizeWarpMesh(mesh, next.columns, next.rows);\n this.mesh = mesh;\n this.changed();\n this.preview();\n }\n\n /** Sets the reference point to an anchor of the frame (the options bar's locator). */\n setReference(anchor: TransformAnchor): void {\n const s = this.session;\n if (!s) return;\n const [fx, fy] = ANCHOR_FRACTION[anchor];\n this.refBox = { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height };\n this.changed();\n }\n\n setInterpolation(interpolation: Interpolation): void {\n const s = this.session;\n if (!s) return;\n this.run(() => s.update({ interpolation }, false));\n }\n\n /** Screen pixels per document pixel and the visible document area, for previews. */\n setView(scale: number, clip?: { x: number; y: number; width: number; height: number }): void {\n this.viewScale = scale > 0 ? scale : 1;\n this.viewClip = clip;\n }\n\n private homography() {\n const s = this.session;\n return s && this.corners ? rectToQuad(s.box, this.corners) : null;\n }\n\n /** The reference point in document pixels. */\n referencePoint(): Vec2 | null {\n const h = this.homography();\n return h ? applyHomography(h, this.refBox) : null;\n }\n\n /** The outline, handles and reference point to draw, or `null` when no transform is open. */\n overlay(): TransformOverlay | null {\n const s = this.session;\n const h = this.homography();\n if (!s || !h || !this.corners) return null;\n const handles = [...CORNERS, ...SIDES].map((id) => {\n const [fx, fy] = HANDLE_FRACTION[id];\n const p = applyHomography(h, { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height }) ?? { x: 0, y: 0 };\n return { id, x: p.x, y: p.y };\n });\n const out: TransformOverlay = { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };\n if (this.modeValue === 'warp' && this.mesh) {\n // Control points through the corner map: exact for affine corners, close for perspective.\n const m = this.mesh;\n const at = (p: Vec2) => applyHomography(h, p) ?? p;\n const pts = m.points.map(at);\n const edit = warpMeshEditPoints(m);\n out.handles = [];\n out.outline = warpMeshGridCurves({ ...m, points: pts }).length ? this.meshOutline(pts, m) : out.outline;\n out.warp = {\n curves: warpMeshGridCurves({ ...m, points: pts }),\n anchors: edit.anchors.map((i) => pts[i]),\n handles: edit.handles.map((e) => ({ point: pts[e.index], anchor: pts[e.anchor] })),\n };\n }\n return out;\n }\n\n /** The mesh's border anchors, clockwise (for the inside test). */\n private meshOutline(pts: Vec2[], m: WarpMesh): Vec2[] {\n const nx = 3 * (m.us.length - 1) + 1;\n const ny = 3 * (m.vs.length - 1) + 1;\n const ring: Vec2[] = [];\n for (let i = 0; i < nx; i++) ring.push(pts[i]);\n for (let j = 1; j < ny; j++) ring.push(pts[j * nx + nx - 1]);\n for (let i = nx - 2; i >= 0; i--) ring.push(pts[(ny - 1) * nx + i]);\n for (let j = ny - 2; j > 0; j--) ring.push(pts[j * nx]);\n return ring;\n }\n\n /** What a document point is over (`tolerance` in document pixels: a few screen pixels / scale). */\n hit(p: ImagePoint, tolerance: number): TransformHandle | null {\n const o = this.overlay();\n if (!o) return null;\n if (this.modeValue === 'warp' && this.mesh) {\n const h = this.homography()!;\n const edit = warpMeshEditPoints(this.mesh);\n let best = -1;\n let bestD = tolerance;\n for (const i of [...edit.anchors, ...edit.handles.map((e) => e.index)]) {\n const q = applyHomography(h, this.mesh.points[i]);\n if (!q) continue;\n const d = Math.hypot(q.x - p.x, q.y - p.y);\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n }\n if (best >= 0) return `mesh:${best}`;\n return insidePolygon(o.outline, p) ? 'inside' : 'outside';\n }\n // The nearest of the handles and the reference point within the tolerance (a small frame on\n // screen puts several within reach); handles win ties.\n let best: TransformHandle | null = null;\n let bestD = tolerance;\n for (const h of o.handles) {\n const d = Math.hypot(h.x - p.x, h.y - p.y);\n if (d <= bestD) {\n best = h.id;\n bestD = d;\n }\n }\n if (Math.hypot(o.reference.x - p.x, o.reference.y - p.y) < bestD) best = 'reference';\n if (best) return best;\n return insideQuad(o.outline, p) ? 'inside' : 'outside';\n }\n\n /** The CSS cursor for a handle. */\n cursor(handle: TransformHandle | null): string {\n if (!handle) return 'default';\n if (meshIndex(handle) >= 0) return 'pointer';\n if (this.modeValue === 'warp') return handle === 'inside' ? 'grab' : 'default';\n if (this.modeValue === 'rotate' && handle !== 'inside' && handle !== 'reference') return CURSOR.outside;\n return CURSOR[handle];\n }\n\n /** Starts a drag on what `p` is over. */\n down(p: ImagePoint, tolerance: number): TransformHandle | null {\n if (!this.session || !this.corners) return null;\n const handle = this.hit(p, tolerance);\n if (!handle) return null;\n let st: { s: number; t: number } | null = null;\n if (this.modeValue === 'warp' && this.mesh && handle === 'inside') {\n const q = this.toFrame(p, this.corners);\n if (q) {\n const r = warpMeshParamAt(this.mesh, q);\n st = { s: r.s, t: r.t };\n }\n }\n this.drag = { handle, start: { x: p.x, y: p.y }, corners: this.corners, refBox: { ...this.refBox }, ref: this.referencePoint() ?? p, mesh: this.mesh, st };\n return handle;\n }\n\n /** A document point in the frame's own space under `corners`. */\n private toFrame(p: Vec2, corners: Quad): Vec2 | null {\n const s = this.session;\n const h: Homography | null = s ? rectToQuad(s.box, corners) : null;\n const inv = h && invertHomography(h);\n return inv ? applyHomography(inv, p) : null;\n }\n\n /** Continues the drag to `p` with the modifiers held now. */\n move(p: ImagePoint, m: TransformModifiers = {}): void {\n const d = this.drag;\n const s = this.session;\n if (!d || !s) return;\n if (d.handle === 'reference') {\n const h = rectToQuad(s.box, d.corners);\n const inv = h && invertHomography(h);\n const q = inv && applyHomography(inv, p);\n if (q) this.refBox = q;\n this.changed();\n return;\n }\n if (this.modeValue === 'warp' && d.mesh) {\n // Warp: mesh points and pulls in the frame's own space; outside the mesh does nothing.\n const a = this.toFrame(d.start, d.corners);\n const b = this.toFrame(p, d.corners);\n if (!a || !b) return;\n const delta = { x: b.x - a.x, y: b.y - a.y };\n const i = meshIndex(d.handle);\n if (i >= 0) this.mesh = dragWarpMeshPoint(d.mesh, i, delta);\n else if (d.handle === 'inside' && d.st) this.mesh = pullWarpMesh(d.mesh, d.st.s, d.st.t, delta);\n else return;\n this.warpSettings = { ...this.warpSettings, style: 'custom' };\n this.changed();\n this.preview();\n return;\n }\n const corners = this.dragCorners(d, p, m);\n if (corners) this.setCorners(corners);\n }\n\n /** Ends the drag (the transform stays open). */\n up(): void {\n this.drag = null;\n }\n\n private dragCorners(d: NonNullable<FreeTransformTool['drag']>, p: Vec2, m: TransformModifiers): Quad | null {\n const s = this.session!;\n const c0 = d.corners;\n let delta = sub(p, d.start);\n const map = (f: (q: Vec2, i: number) => Vec2) => c0.map(f) as unknown as Quad;\n const handle = d.handle;\n const isCorner = handle in CORNER_INDEX;\n const isSide = handle in SIDE_CORNERS;\n const rotating = handle === 'outside' || (this.modeValue === 'rotate' && (isCorner || isSide));\n if (handle === 'inside') {\n if (m.shiftKey) delta = Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };\n return map((q) => add(q, delta));\n }\n if (rotating) {\n const r = d.ref;\n let a = Math.atan2(p.y - r.y, p.x - r.x) - Math.atan2(d.start.y - r.y, d.start.x - r.x);\n if (m.shiftKey) a = Math.round(a / (Math.PI / 12)) * (Math.PI / 12);\n return map((q) => rotateAbout(q, r, a));\n }\n const perspective = this.modeValue === 'perspective' || (mod(m) && m.altKey && m.shiftKey);\n const distort = this.modeValue === 'distort' || (mod(m) && !perspective);\n const skew = this.modeValue === 'skew' || (mod(m) && isSide);\n if (isSide && (skew || perspective)) {\n // Slide the side along its own direction (Alt: the opposite side the other way).\n const [i, j] = SIDE_CORNERS[handle];\n const dir = sub(c0[j], c0[i]);\n const len = Math.hypot(dir.x, dir.y) || 1;\n const t = (delta.x * dir.x + delta.y * dir.y) / len;\n const v = { x: (dir.x / len) * t, y: (dir.y / len) * t };\n const [oi, oj] = SIDE_CORNERS[{ n: 's', s: 'n', e: 'w', w: 'e' }[handle as 'n']];\n return map((q, k) => (k === i || k === j ? add(q, v) : m.altKey && (k === oi || k === oj) ? sub(q, v) : q));\n }\n if (isCorner && perspective) {\n const k = CORNER_INDEX[handle];\n if (Math.abs(delta.x) >= Math.abs(delta.y)) {\n const partner = [1, 0, 3, 2][k];\n return map((q, i) => (i === k ? { x: q.x + delta.x, y: q.y } : i === partner ? { x: q.x - delta.x, y: q.y } : q));\n }\n const partner = [3, 2, 1, 0][k];\n return map((q, i) => (i === k ? { x: q.x, y: q.y + delta.y } : i === partner ? { x: q.x, y: q.y - delta.y } : q));\n }\n if (distort || (isCorner && this.modeValue === 'skew')) {\n if (isCorner) return map((q, i) => (i === CORNER_INDEX[handle] ? add(q, delta) : q));\n const [i, j] = SIDE_CORNERS[handle];\n return map((q, k) => (k === i || k === j ? add(q, delta) : q));\n }\n // Scale in the frame's own space, so a rotated or distorted frame scales along its sides.\n const h = rectToQuad(s.box, c0);\n const inv = h && invertHomography(h);\n if (!h || !inv) return null;\n const q = applyHomography(inv, p);\n const q0 = applyHomography(inv, d.start);\n if (!q || !q0) return null;\n const b = s.box;\n const [fx, fy] = HANDLE_FRACTION[handle];\n const grab = { x: b.x + fx * b.width, y: b.y + fy * b.height };\n const anchor = m.altKey ? d.refBox : { x: b.x + (1 - fx) * b.width, y: b.y + (1 - fy) * b.height };\n const target = add(grab, sub(q, q0));\n const ax = grab.x - anchor.x;\n const ay = grab.y - anchor.y;\n let sx = isCorner || handle === 'e' || handle === 'w' ? (Math.abs(ax) > 1e-9 ? (target.x - anchor.x) / ax : 1) : 1;\n let sy = isCorner || handle === 'n' || handle === 's' ? (Math.abs(ay) > 1e-9 ? (target.y - anchor.y) / ay : 1) : 1;\n // Corners scale proportionally by default; Shift frees them.\n if (isCorner && !m.shiftKey) {\n const k = Math.abs(sx) >= Math.abs(sy) ? sx : sy;\n sx = k;\n sy = k;\n }\n const scaled = [\n { x: b.x, y: b.y },\n { x: b.x + b.width, y: b.y },\n { x: b.x + b.width, y: b.y + b.height },\n { x: b.x, y: b.y + b.height },\n ].map((c) => applyHomography(h, { x: anchor.x + (c.x - anchor.x) * sx, y: anchor.y + (c.y - anchor.y) * sy }));\n return scaled.every(Boolean) ? (scaled as unknown as Quad) : null;\n }\n\n /** Sets new corners and refreshes the preview (scheduled). Invalid shapes keep the last good one. */\n setCorners(corners: Quad): void {\n const s = this.session;\n if (!s) return;\n const h = rectToQuad(s.box, corners);\n if (!h) return;\n this.corners = corners;\n this.typed = {};\n this.changed();\n this.preview();\n }\n\n private preview(): void {\n if (this.pending) return;\n this.pending = true;\n const run = () => {\n this.pending = false;\n const s = this.session;\n if (!s || !this.corners) return;\n this.run(() => s.update({ corners: this.corners!, warp: this.mesh }, { scale: this.viewScale, clip: this.viewClip }));\n };\n if (this.options.schedule) this.options.schedule(run);\n else run();\n }\n\n private run(fn: () => void): void {\n try {\n fn();\n this.error = null;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n if (this.sessionRef && !this.sessionRef.isOpen) this.sessionRef = null;\n }\n this.changed();\n }\n\n /** The options bar's numbers for the current shape. */\n bar(): FreeTransformBar | null {\n const s = this.session;\n if (!s || !this.corners) return null;\n const read: TransformReadout = describeTransform({ box: s.box, corners: this.corners }, this.refBox);\n return {\n affine: read.affine,\n x: this.typed.x ?? read.x,\n y: this.typed.y ?? read.y,\n scaleX: this.typed.scaleX ?? read.scaleX,\n scaleY: this.typed.scaleY ?? read.scaleY,\n rotate: this.typed.rotate ?? read.rotate,\n skewX: this.typed.skewX ?? read.skewX,\n skewY: this.typed.skewY ?? 0,\n };\n }\n\n /** Applies a typed options-bar value (W and H together while `proportional`). */\n setField(field: FreeTransformField, value: number): void {\n const s = this.session;\n const cur = this.bar();\n if (!s || !cur || !Number.isFinite(value)) return;\n const next = { ...cur, [field]: value };\n if (this.proportional && field === 'scaleX') next.scaleY = cur.scaleY === 0 ? value : (value / cur.scaleX) * cur.scaleY;\n if (this.proportional && field === 'scaleY') next.scaleX = cur.scaleX === 0 ? value : (value / cur.scaleY) * cur.scaleX;\n if (!next.scaleX || !next.scaleY) {\n this.error = 'Width and height must not be 0 %.';\n this.changed();\n return;\n }\n const m = matrixFromParams(s.box, {\n scaleX: next.scaleX,\n scaleY: next.scaleY,\n rotate: next.rotate,\n skewX: next.skewX,\n skewY: next.skewY,\n reference: this.refBox,\n dx: next.x - this.refBox.x,\n dy: next.y - this.refBox.y,\n });\n this.setCorners(cornersFromMatrix(s.box, m));\n this.typed = { scaleX: next.scaleX, scaleY: next.scaleY, rotate: next.rotate, skewX: next.skewX, skewY: next.skewY, x: next.x, y: next.y };\n this.changed();\n }\n\n /** Finishes with full-quality resampling (one undo step). */\n commit(): FreeTransformResult | null {\n const s = this.session;\n if (!s) return null;\n let res: FreeTransformResult | null = null;\n this.pending = false;\n this.run(() => {\n if (this.corners) s.update({ corners: this.corners, warp: this.mesh }, false);\n res = s.commit();\n });\n this.lastResult = res;\n this.sessionRef = null;\n this.corners = null;\n this.mesh = null;\n this.drag = null;\n this.changed();\n return res;\n }\n\n /** Abandons the transform and restores everything. */\n cancel(): void {\n this.sessionRef?.cancel();\n this.sessionRef = null;\n this.corners = null;\n this.mesh = null;\n this.drag = null;\n this.pending = false;\n this.error = null;\n this.changed();\n }\n\n /**\n * Keyboard: Enter commits, Escape cancels, arrows nudge by 1 px (Shift: 10). Ctrl/Cmd+T opens a\n * Free Transform when none is open. Returns true when the key was used (prevent its default).\n */\n key(e: ShortcutEvent): boolean {\n const t = e.target as { tagName?: string; isContentEditable?: boolean } | null;\n const typing = Boolean(t && (t.isContentEditable || t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT'));\n if (!this.active) {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 't' && !typing) {\n this.begin();\n return true;\n }\n return false;\n }\n if (e.key === 'Enter') {\n this.commit();\n return true;\n }\n if (e.key === 'Escape') {\n this.cancel();\n return true;\n }\n if (typing) return false;\n const step = e.shiftKey ? 10 : 1;\n const nudge: Record<string, Vec2> = { ArrowLeft: { x: -step, y: 0 }, ArrowRight: { x: step, y: 0 }, ArrowUp: { x: 0, y: -step }, ArrowDown: { x: 0, y: step } };\n const v = nudge[e.key];\n if (v && this.corners) {\n this.setCorners(this.corners.map((q) => add(q, v)) as unknown as Quad);\n return true;\n }\n return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","../../src/image-editor/histogram.ts","../../src/image-editor/selection/overlay.ts","../../src/image-editor/layers/migrate.ts","../../src/image-editor/layers/render.ts","../../src/image-editor/renderer.ts","../../src/image-editor/selection/shapes.ts","../../src/image-editor/selection/wand.ts","../../src/image-editor/selection/refine.ts","../../src/image-editor/free-transform.ts","../../src/image-editor/controller.ts","../../src/image-editor/document.ts","../../src/image-editor/controls.ts","../../src/image-editor/selection/tool-options.ts","../../src/image-editor/selection-tools.ts","../../src/image-editor/capture.ts","../../src/image-editor/edit-tools.ts","../../src/image-editor/layer-tools.ts","../../src/image-editor/effect-tools.ts","../../src/image-editor/layers/paint-ui.ts","../../src/image-editor/paint-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/transform-tools.ts","../../src/image-editor/smart-tools.ts","../../src/image-editor/tools.ts","../../src/image-editor/curve-editing.ts","../../src/image-editor/selection/gesture.ts","../../src/image-editor/mosaic-filter.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/effects-ui.ts","../../src/image-editor/psd/ui.ts","../../src/image-editor/layers/transform-ui.ts"],"names":["px","b","data","out","pct","WRITE_IDEMPOTENT","LAYER_ID","opt","TARGET","summary","id","spec","bestD","q","partner"],"mappings":"AAAA;AACE;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACA;AChcO,IAAM,eAAA,EAAiB,GAAA;AAgCvB,SAAS,gBAAA,CAAiB,SAAA,EAAwC;AACvE,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AA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACC,MAAA;AAEf,IAAA;AACL,IAAA;AACe,IAAA;AACI,IAAA;AACrB,EAAA;AACyC,EAAA;AACN,EAAA;AACrB,EAAA;AACI,IAAA;AACC,MAAA;AACJ,MAAA;AACA,MAAA;AACb,IAAA;AACF,EAAA;AACW,EAAA;AACU,EAAA;AACD,EAAA;AACgB,EAAA;AAG3B,EAAA;AACX;AAG8B;AACT,EAAA;AACA,EAAA;AACL,EAAA;AACA,EAAA;AACK,EAAA;AACX,EAAA;AACW,EAAA;AACE,EAAA;AACD,EAAA;AACC,EAAA;AACH,EAAA;AACL,EAAA;AACE,IAAA;AACM,IAAA;AACA,IAAA;AACrB,EAAA;AACQ,EAAA;AACU,EAAA;AACA,EAAA;AACE,IAAA;AACC,IAAA;AACnB,IAAA;AACF,EAAA;AACkB,EAAA;AACE,IAAA;AACH,IAAA;AACjB,EAAA;AACY,EAAA;AACF,EAAA;AACH,EAAA;AACT;AHibuB;AACA;AI3wBO;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;AAQU;AACC,EAAA;AACD,EAAA;AACK,IAAA;AACF,IAAA;AACV,IAAA;AACP,EAAA;AACO,EAAA;AACa,IAAA;AACD,IAAA;AACnB,EAAA;AACF;AAG6B;AACN,EAAA;AACA,EAAA;AACN,EAAA;AACjB;AAQgB;AAOK,EAAA;AACD,IAAA;AAClB,EAAA;AACkB,EAAA;AACN,EAAA;AACK,EAAA;AACV,EAAA;AACT;AAOgB;AACP,EAAA;AACI,IAAA;AACE,IAAA;AACD,IAAA;AACK,IAAA;AACN,IAAA;AACI,IAAA;AACI,IAAA;AACE,IAAA;AACd,IAAA;AACP,EAAA;AACF;AAGgB;AACO,EAAA;AACN,IAAA;AACG,IAAA;AACE,MAAA;AACZ,QAAA;AACF,QAAA;AACF,MAAA;AACE,MAAA;AACc,MAAA;AACD,MAAA;AACA,MAAA;AACR,MAAA;AACE,MAAA;AACD,MAAA;AACS,MAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACnB,IAAA;AACF,EAAA;AACF;AAG8B;AAChB,EAAA;AACI,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACC,MAAA;AACF,MAAA;AACjB,IAAA;AACiB,IAAA;AACE,IAAA;AACH,IAAA;AACD,MAAA;AACG,MAAA;AAClB,IAAA;AACF,EAAA;AACU,EAAA;AACU,EAAA;AACV,EAAA;AACH,EAAA;AACT;AJksBuB;AACA;AKj2BN;AACC,iBAAA;AAEyC,EAAA;AACtC,IAAA;AACD,IAAA;AACP,MAAA;AACT,IAAA;AAEmB,IAAA;AACN,MAAA;AACE,QAAA;AACX,QAAA;AACF,MAAA;AACgB,MAAA;AACZ,MAAA;AACS,MAAA;AACI,QAAA;AACJ,QAAA;AACb,MAAA;AACe,MAAA;AACL,MAAA;AAGC,MAAA;AACE,QAAA;AACL,UAAA;AACK,YAAA;AACA,YAAA;AACA,YAAA;AACD,UAAA;AAER,UAAA;AACW,UAAA;AACA,UAAA;AACR,QAAA;A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AA;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;APugEuB;AACA;AQpyEV;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,EAA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;AACe,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACZ,IAAA;AACT,EAAA;AACF;AAMS;AAC4B,EAAA;AACnB,EAAA;AACA,EAAA;AACZ,EAAA;AACS,IAAA;AACI,IAAA;AACjB,EAAA;AACO,EAAA;AACT;AAGqB;AACE,EAAA;AACL,EAAA;AACF,EAAA;AACI,IAAA;AACJ,MAAA;AACJ,MAAA;AACG,MAAA;AACG,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACG,MAAA;AACV,IAAA;AACG,IAAA;AACN,EAAA;AACmB,EAAA;AACH,EAAA;AACK,IAAA;AACF,IAAA;AACnB,EAAA;AACiB,EAAA;AACJ,EAAA;AACM,EAAA;AACrB;AAGkB;AACC,EAAA;AACe,EAAA;AAChB,EAAA;AACD,IAAA;AACG,MAAA;AACR,MAAA;AACG,MAAA;AACT,MAAA;AACD,IAAA;AACH,EAAA;AACkB,EAAA;AACE,EAAA;AACH,EAAA;AACV,EAAA;AACT;AAGS;AACM,EAAA;AACR,IAAA;AACU,IAAA;AACM,IAAA;AACH,IAAA;AACA,IAAA;AACF,IAAA;AACD,IAAA;AACf,EAAA;AACkB,EAAA;AACb,EAAA;AACa,IAAA;AACH,IAAA;AACE,IAAA;AACD,MAAA;AACD,QAAA;AACA,QAAA;AACA,QAAA;AACV,MAAA;AACI,IAAA;AACK,MAAA;AACE,MAAA;AACK,MAAA;AACnB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGuB;AACS,EAAA;AACpB,EAAA;AACW,IAAA;AACD,IAAA;AACD,IAAA;AACnB,EAAA;AACU,EAAA;AACW,IAAA;AACP,IAAA;AACK,IAAA;AACnB,EAAA;AACU,EAAA;AACW,IAAA;AACT,IAAA;AACZ,EAAA;AACqB,EAAA;AACD,IAAA;AACC,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAGS;AACa,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACH,EAAA;AACP,EAAA;AACM,EAAA;AACC,IAAA;AACD,IAAA;AACD,IAAA;AACF,IAAA;AACI,IAAA;AACjB,EAAA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,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,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;AACv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AAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACI,IAAA;AACN,MAAA;AACK,MAAA;AACC,MAAA;AACR,MAAA;AACX,IAAA;AACF,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACH,IAAA;AACC,MAAA;AACF,MAAA;AACX,IAAA;AACF,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AjBo4OuB;AACA;AkBh+PR;AACC;AACVD;AACgB;AAEH;AACC;AACF;AAGL;AACI,EAAA;AACN,EAAA;AACD,IAAA;AACO,IAAA;AACD,IAAA;AACI,MAAA;AACN,MAAA;AACA,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACD,YAAA;AACC,YAAA;AACA,YAAA;AACZ,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACmB,IAAA;AACnB,IAAA;AACF,EAAA;AACU,EAAA;AACF,IAAA;AACO,IAAA;AACD,IAAA;AACH,MAAA;AACC,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACD,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACc,MAAA;AACN,QAAA;AACO,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACA,YAAA;AACC,YAAA;AACb,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACe,MAAA;AACA,MA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AAA;AAG+B,EAAA;AACd,IAAA;AACC,IAAA;AACe,IAAA;AACxB,IAAA;AACQ,MAAA;AACC,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AACd,IAAA;AACF,EAAA;AAAA;AAGoC,EAAA;AACnB,IAAA;AACE,IAAA;AACE,IAAA;AACD,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;A/BolUuB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","sourcesContent":[null,"// Portions ported from photocraft-main/crates/algo/src/histogram.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Display bins of a histogram. */\nexport const HISTOGRAM_BINS = 256;\n\nexport interface ImageHistogram {\n /** 256-bin counts per channel. */\n red: Uint32Array;\n green: Uint32Array;\n blue: Uint32Array;\n /** 256-bin counts of Rec.709 luma `round(0.2126 R + 0.7152 G + 0.0722 B)` of the encoded values. */\n luminance: Uint32Array;\n /** Visible pixels counted (alpha > 0). Each counts once, partially transparent ones included. */\n samples: number;\n /** Fully transparent pixels, excluded from every bin. */\n transparent: number;\n /** Per channel (R, G, B): visible pixels at exactly 0. */\n shadows: [number, number, number];\n /** Per channel (R, G, B): visible pixels at exactly 255. */\n highlights: [number, number, number];\n /** Largest bin count over R, G, B and luminance (for scaling a plot). */\n max: number;\n}\n\ninterface PixelSource {\n data: ArrayLike<number>;\n width?: number;\n height?: number;\n}\n\n/**\n * Per-channel and luminance histograms of straight 8-bit RGBA pixels (an `ImageData` or anything\n * with `data`), with clipping counts at the exact endpoints. Transparent pixels cannot add\n * phantom peaks: they are counted separately and excluded.\n */\nexport function computeHistogram(imageData: PixelSource): ImageHistogram {\n const red = new Uint32Array(HISTOGRAM_BINS);\n const green = new Uint32Array(HISTOGRAM_BINS);\n const blue = new Uint32Array(HISTOGRAM_BINS);\n const luminance = new Uint32Array(HISTOGRAM_BINS);\n const shadows: [number, number, number] = [0, 0, 0];\n const highlights: [number, number, number] = [0, 0, 0];\n let samples = 0;\n let transparent = 0;\n const data = imageData.data;\n const len = data.length - (data.length % 4);\n for (let i = 0; i < len; i += 4) {\n if (!(data[i + 3] > 0)) {\n transparent++;\n continue;\n }\n samples++;\n const r = clampByte(data[i]);\n const g = clampByte(data[i + 1]);\n const b = clampByte(data[i + 2]);\n red[r]++;\n green[g]++;\n blue[b]++;\n luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) shadows[0]++;\n if (g === 0) shadows[1]++;\n if (b === 0) shadows[2]++;\n if (r === 255) highlights[0]++;\n if (g === 255) highlights[1]++;\n if (b === 255) highlights[2]++;\n }\n let max = 0;\n for (const bins of [red, green, blue, luminance]) {\n for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];\n }\n return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };\n}\n\nfunction clampByte(v: number): number {\n return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);\n}\n\nexport type HistogramChannel = 'red' | 'green' | 'blue' | 'luminance';\n\n/**\n * An SVG path (`M … Z`) of one channel scaled into a `width` × `height` box, bins left to right,\n * counts up from the bottom. `scale: 'sqrt'` (default) keeps small populations visible.\n */\nexport function histogramPath(\n histogram: ImageHistogram,\n channel: HistogramChannel,\n width: number,\n height: number,\n options: { scale?: 'linear' | 'sqrt'; max?: number } = {}\n): string {\n const bins = histogram[channel];\n const sqrt = (options.scale ?? 'sqrt') === 'sqrt';\n const f = (v: number) => (sqrt ? Math.sqrt(v) : v);\n const top = f(options.max ?? histogram.max) || 1;\n const dx = width / (HISTOGRAM_BINS - 1);\n let d = `M0 ${height}`;\n for (let k = 0; k < HISTOGRAM_BINS; k++) {\n const y = height - (f(bins[k]) / top) * height;\n d += ` L${round2(k * dx)} ${round2(y)}`;\n }\n return `${d} L${round2(width)} ${height} Z`;\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { SelectionMask } from './mask';\n\n/**\n * The selection's outline at `threshold` (0..255, default 128) as axis-aligned segments on pixel\n * boundaries, `[x0, y0, x1, y1, …]` in natural image pixels, with collinear neighbours merged.\n * This is what marching ants are drawn from. Linear in the pixel count.\n */\nexport function selectionOutline(mask: SelectionMask, threshold = 128): Float32Array {\n const { width: w, height: h, data } = mask;\n const segs: number[] = [];\n if (!mask.bounds) return new Float32Array(0);\n const b = mask.bounds;\n const inside = (x: number, y: number) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;\n // Horizontal boundaries: the line y separates rows y − 1 and y.\n for (let y = b.y; y <= b.y + b.height; y++) {\n let start = -1;\n for (let x = b.x; x <= b.x + b.width; x++) {\n const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);\n if (edge && start < 0) start = x;\n else if (!edge && start >= 0) {\n segs.push(start, y, x, y);\n start = -1;\n }\n }\n }\n // Vertical boundaries: the line x separates columns x − 1 and x.\n for (let x = b.x; x <= b.x + b.width; x++) {\n let start = -1;\n for (let y = b.y; y <= b.y + b.height; y++) {\n const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);\n if (edge && start < 0) start = y;\n else if (!edge && start >= 0) {\n segs.push(x, start, x, y);\n start = -1;\n }\n }\n }\n return Float32Array.from(segs);\n}\n\nexport interface QuickMaskStyle {\n /** Overlay colour, `[r, g, b]` 0..255 (default Photoshop's rubylith red). */\n color?: readonly [number, number, number];\n /** 0..1 (default 0.5): opacity over fully unselected pixels. */\n opacity?: number;\n}\n\n/**\n * Quick Mask view: RGBA pixels (natural image size) tinting the *unselected* area, so the\n * selection shows through clear, as Photoshop's rubylith overlay.\n */\nexport function quickMaskPixels(mask: SelectionMask, style: QuickMaskStyle = {}): Uint8ClampedArray {\n const [r, g, b] = style.color ?? [255, 0, 0];\n const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n out[p] = r;\n out[p + 1] = g;\n out[p + 2] = b;\n out[p + 3] = Math.round((255 - mask.data[i]) * k);\n }\n return out;\n}\n","/**\n * Between the old document-level model (schema v1 / v2: base sliders, adjustment layers, blur,\n * depth mask and annotations) and the layer stack:\n *\n * - `layersFromLegacy` lifts a v1 / v2 state into layers: the source becomes the Background\n * pixel layer carrying the base sliders as its `develop` filter and the depth mask as its\n * parametric mask; adjustment layers go above it, annotations on top in their order. Blur\n * becomes a `blur` filter on the Background, or `legacyOutputBlur` when adjustment layers\n * exist (blur used to run after them).\n * - `legacyView` reads the stack back as `adjustments` / `depthMask` / `annotations` (the\n * compatibility views on the editor state) and says whether the stack still has exactly the\n * legacy shape, in which case it serializes as v1 / v2 byte for byte.\n */\n\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport { normalizeAdjustmentLayers } from '../adjustment-layers';\nimport { normalizeProcessVersion } from '../color';\nimport { decodeLayerMask, normalizeLayerMask } from '../selection/layer-mask';\nimport { encodeSelectionRle, resizeSelection } from '../selection/mask';\nimport type {\n AdjustmentLayer,\n AdjustmentLayerMask,\n Annotation,\n DepthMaskOptions,\n ImageAdjustments,\n} from '../types';\nimport {\n blurFilter,\n developFilter,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { emptySurface, readRegion, surfaceFromPixels, type MaskSurface } from '../../raster/surface';\n\nexport const BACKGROUND_LAYER_ID = 'background';\nexport const BACKGROUND_LAYER_NAME = 'Background';\nexport const DEVELOP_FILTER_ID = 'develop';\nexport const BLUR_FILTER_ID = 'blur';\n\n/** The old document-level parts of the editor state. */\nexport interface LegacyParts {\n adjustments: ImageAdjustments;\n depthMask: DepthMaskOptions;\n annotations: Annotation[];\n}\n\n/** The stack as stored at document level next to the layers. */\nexport interface LayerStack {\n layers: ImageLayer[];\n /** Export alpha (the old `adjustments.opacity`), 0..1. */\n outputOpacity: number;\n /** Migrated blur that must run after the adjustment layers (never created by new edits). */\n legacyOutputBlur?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Masks\n// ---------------------------------------------------------------------------\n\n/**\n * Stored `r1:` encodings of masks lifted from v1 / v2, by surface, so an unchanged mask writes\n * back byte for byte (and a mask of another size than the document keeps its size).\n */\nconst legacyMaskMemo = new WeakMap<MaskSurface, { width: number; height: number; rle: string }>();\n\n/** A raster mask from an old adjustment-layer mask, resampled to the document if needed. */\nexport function rasterMaskFromLegacy(mask: AdjustmentLayerMask, width: number, height: number): RasterMask | null {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);\n legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });\n const out: RasterMask = { surface, defaultValue: 0 };\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\n/**\n * True when an adjustment layer's mask came from a v1 / v2 mask of another size than the canvas\n * (the old renderer resampled it per render; the stack holds a canvas-sized copy).\n */\nexport function hasResampledLegacyMask(layers: readonly ImageLayer[], width: number, height: number): boolean {\n for (const l of layers) {\n if (l.type !== 'adjustment' || !l.mask) continue;\n const memo = legacyMaskMemo.get(l.mask.surface);\n if (memo && (memo.width !== width || memo.height !== height)) return true;\n }\n return false;\n}\n\n/** The old mask shape of a raster mask (its pixels over the canvas, run-length encoded). */\nexport function legacyMaskOf(mask: RasterMask, width: number, height: number): AdjustmentLayerMask {\n let memo = legacyMaskMemo.get(mask.surface);\n if (!memo) {\n const data = readRegion(mask.surface, { x: 0, y: 0, width, height });\n memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };\n legacyMaskMemo.set(mask.surface, memo);\n }\n return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled })!;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy → layers\n// ---------------------------------------------------------------------------\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** True when the depth mask settings are the defaults (no mask at all). */\nexport function isDefaultDepthMask(mask: DepthMaskOptions | undefined): boolean {\n if (!mask) return true;\n return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...(mask.depthRange ?? [])] }, DEFAULT_DEPTH_MASK_VALUES);\n}\n\nexport function developFromAdjustments(adjustments: ImageAdjustments): DevelopFilter {\n return {\n id: DEVELOP_FILTER_ID,\n kind: 'develop',\n enabled: true,\n brightness: adjustments.brightness ?? 0,\n contrast: adjustments.contrast ?? 0,\n saturation: adjustments.saturation ?? 0,\n exposure: adjustments.exposure ?? 0,\n temperature: adjustments.temperature ?? 0,\n tint: adjustments.tint ?? 0,\n process: normalizeProcessVersion(adjustments.process),\n };\n}\n\n/** The default name of an annotation layer. */\nexport function annotationLayerName(annotation: Annotation): string {\n switch (annotation.type) {\n case 'pen':\n return 'Pen';\n case 'rect':\n return 'Rectangle';\n case 'circle':\n return 'Ellipse';\n case 'arrow':\n return 'Arrow';\n case 'line':\n return 'Line';\n case 'text':\n return annotation.text ? annotation.text.slice(0, 32) : 'Text';\n case 'image':\n return 'Image';\n }\n}\n\nexport function annotationLayerFor(annotation: Annotation): AnnotationLayer {\n return {\n type: 'annotation',\n id: annotation.id,\n name: annotationLayerName(annotation),\n visible: true,\n opacity: 1,\n blend: 'normal',\n annotation,\n };\n}\n\nexport function adjustmentNodeFromLegacy(layer: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const node = {\n type: 'adjustment',\n id: layer.id,\n name: layer.name ?? '',\n visible: layer.enabled !== false,\n opacity: layer.opacity ?? 1,\n blend: layer.blend ?? 'normal',\n kind: layer.kind,\n params: JSON.parse(JSON.stringify(layer.params)),\n } as AdjustmentLayerNode;\n if (layer.mask) {\n const mask = rasterMaskFromLegacy(layer.mask, width, height);\n if (mask) node.mask = mask;\n }\n return node;\n}\n\n/** The Background layer of an opened image (its pixels are the document source). */\nexport function backgroundLayer(filters: LayerFilter[] = [], mask?: RasterMask): PixelLayer {\n const layer: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n source: 'document',\n filters,\n };\n if (mask) layer.mask = mask;\n return layer;\n}\n\n/** A parametric depth mask on a layer. */\nexport function depthRasterMask(source: DepthMaskOptions): RasterMask {\n const out: RasterMask = {\n surface: emptySurface(1, 255),\n defaultValue: 255,\n source: { ...source, depthRange: [...source.depthRange] as [number, number] },\n };\n if (!source.enabled) out.enabled = false;\n return out;\n}\n\n/** Lifts a v1 / v2 state into a layer stack (see the module comment). */\nexport function layersFromLegacy(parts: Partial<LegacyParts>, width: number, height: number): LayerStack {\n // As `normalizeStoredAdjustments`: stored values without `process` keep the legacy rendering.\n const stored = parts.adjustments;\n const adjustments = (\n stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES }\n ) as ImageAdjustments;\n const adjLayers = normalizeAdjustmentLayers(adjustments.layers);\n const filters: LayerFilter[] = [developFromAdjustments(adjustments)];\n const blur = typeof adjustments.blur === 'number' && adjustments.blur > 0 ? adjustments.blur : 0;\n let legacyOutputBlur: number | undefined;\n if (blur > 0) {\n if (adjLayers.length) legacyOutputBlur = blur;\n else filters.push({ id: BLUR_FILTER_ID, kind: 'blur', enabled: true, radius: blur });\n }\n const depth = parts.depthMask;\n const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : undefined;\n const layers: ImageLayer[] = [backgroundLayer(filters, mask)];\n for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));\n for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));\n const out: LayerStack = { layers, outputOpacity: typeof adjustments.opacity === 'number' ? adjustments.opacity : 1 };\n if (legacyOutputBlur !== undefined) out.legacyOutputBlur = legacyOutputBlur;\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Layers → legacy views\n// ---------------------------------------------------------------------------\n\n/** The old `AdjustmentLayer` shape of a node, keys in the order `normalizeAdjustmentLayer` writes. */\nexport function legacyAdjustmentLayer(node: AdjustmentLayerNode, width: number, height: number): AdjustmentLayer {\n const out: Record<string, unknown> = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n };\n if (node.name) out.name = node.name;\n if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);\n return out as unknown as AdjustmentLayer;\n}\n\n/** The pixel layer whose develop filter the legacy API edits: the active one, else the Background. */\nexport function developTarget(layers: readonly ImageLayer[], activeLayerId: string | null): PixelLayer | null {\n const pixels: PixelLayer[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'pixel') pixels.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (activeLayerId) {\n const active = pixels.find((l) => l.id === activeLayerId);\n if (active) return active;\n }\n return pixels.find((l) => l.source === 'document') ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;\n}\n\nexport interface LegacyViewResult extends LegacyParts {\n /**\n * True when the stack has exactly the shape a v1 / v2 document lifts to (an unpainted,\n * unmoved Background with develop / blur filters and an optional depth mask, then adjustment\n * layers, then plain annotation layers), so it can be written as v1 / v2.\n */\n exact: boolean;\n}\n\nconst isPlain = (l: ImageLayer) =>\n l.visible && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask && !l.locks && !l.effects;\n\n/** Reads the stack back as the old document-level parts (see `LegacyViewResult`). */\nexport function legacyView(stack: LayerStack, width: number, height: number, activeLayerId: string | null = null): LegacyViewResult {\n const { layers } = stack;\n const target = developTarget(layers, activeLayerId);\n const develop = target ? developFilter(target) : undefined;\n const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : undefined) ?? 0;\n const adjustments: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS_VALUES,\n ...(develop\n ? {\n brightness: develop.brightness,\n contrast: develop.contrast,\n saturation: develop.saturation,\n exposure: develop.exposure,\n temperature: develop.temperature,\n tint: develop.tint ?? 0,\n }\n : {}),\n blur,\n opacity: stack.outputOpacity,\n process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process,\n };\n const adjNodes: AdjustmentLayerNode[] = [];\n const annotations: Annotation[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'adjustment') adjNodes.push(l);\n else if (l.type === 'annotation') annotations.push(l.annotation);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));\n const depthSource = target?.mask?.source;\n const depthMask: DepthMaskOptions = depthSource\n ? { ...depthSource, depthRange: [...depthSource.depthRange] as [number, number] }\n : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] as [number, number] };\n return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };\n}\n\n/** See `LegacyViewResult.exact`. */\nexport function isLegacyShape(stack: LayerStack): boolean {\n const { layers } = stack;\n const bg = layers[0];\n if (!bg || bg.type !== 'pixel' || bg.source !== 'document') return false;\n if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;\n if (!bg.visible || bg.opacity !== 1 || bg.blend !== 'normal' || bg.clipped || bg.locks || bg.effects) return false;\n if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;\n const filters = bg.filters ?? [];\n if (filters.length < 1 || filters.length > 2) return false;\n const [develop, blur] = filters;\n if (develop.kind !== 'develop' || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;\n if (blur && (blur.kind !== 'blur' || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;\n if (bg.mask) {\n const m = bg.mask;\n if (!m.source || m.density !== undefined || m.feather !== undefined) return false;\n if ((m.enabled !== false) !== m.source.enabled) return false;\n }\n let i = 1;\n let adjustments = 0;\n for (; i < layers.length && layers[i].type === 'adjustment'; i++) {\n const l = layers[i] as AdjustmentLayerNode;\n if (l.clipped || l.locks || l.mask?.source || l.effects) return false;\n adjustments++;\n }\n for (; i < layers.length; i++) {\n const l = layers[i];\n if (l.type !== 'annotation' || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;\n }\n if (blur && adjustments) return false;\n if (stack.legacyOutputBlur !== undefined && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;\n return true;\n}\n","/**\n * Pure helpers between the compositor and the renderer: the output stage (projection to the\n * cropped, rotated frame, output opacity and the migrated blur), the routing rule that keeps\n * old-shape documents on the exact legacy path where document-space compositing cannot match it,\n * and per-tile histograms. No DOM.\n */\n\nimport { applyBoxBlur } from '../filters';\nimport { normalizeAngle, unprojectCanvasPoint, getRotatedSize, type ImageProjection } from '../geometry';\nimport { HISTOGRAM_BINS, type ImageHistogram } from '../histogram';\nimport type { CropBox, ImageTransform } from '../types';\nimport { blurFilter, type AnnotationLayer, type ImageLayer, type PixelLayer } from './model';\nimport { hasResampledLegacyMask, isLegacyShape, type LayerStack } from './migrate';\nimport { LayerCompositor, type CompositorSources } from './compositor';\n\n/** True for 0, 90, 180 and 270 degrees. */\nexport function isQuarterTurn(rotate: number): boolean {\n return normalizeAngle(rotate) % 90 === 0;\n}\n\nfunction isIntegerRect(r: CropBox): boolean {\n return Number.isInteger(r.x) && Number.isInteger(r.y) && Number.isInteger(r.width) && Number.isInteger(r.height);\n}\n\n/** The projection that draws `region` into a `baseW × baseH` buffer at scale 1 (the renderer's base canvas). */\nexport function baseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/**\n * Projects `src` (RGBA covering `srcRect` in document space) into the base frame of `region`\n * under a quarter-turn `transform`: an exact pixel permutation (crop, flips, 90° steps), the\n * same mapping the canvas draws with. Returns the base buffer and its size.\n */\nexport function projectQuarterTurn(\n src: ArrayLike<number>,\n srcRect: CropBox,\n region: CropBox,\n transform: ImageTransform\n): { width: number; height: number; data: Uint8ClampedArray } {\n const rotated = getRotatedSize(region.width, region.height, transform);\n const w = Math.max(1, Math.round(rotated.width));\n const h = Math.max(1, Math.round(rotated.height));\n const out = new Uint8ClampedArray(w * h * 4);\n const p = baseProjection(region, transform, w, h);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, p);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, p);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, p);\n const ux = Math.round(ox.x - o.x);\n const vx = Math.round(ox.y - o.y);\n const uy = Math.round(oy.x - o.x);\n const vy = Math.round(oy.y - o.y);\n const u0 = Math.floor(o.x) - srcRect.x;\n const v0 = Math.floor(o.y) - srcRect.y;\n const sw = srcRect.width;\n const sh = srcRect.height;\n for (let y = 0; y < h; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n for (let x = 0; x < w; x++, u += ux, v += vx) {\n if (u < 0 || v < 0 || u >= sw || v >= sh) continue;\n const s = (v * sw + u) * 4;\n const d = (y * w + x) * 4;\n out[d] = src[s];\n out[d + 1] = src[s + 1];\n out[d + 2] = src[s + 2];\n out[d + 3] = src[s + 3];\n }\n }\n return { width: w, height: h, data: out };\n}\n\n/** Output opacity as the old base adjustments applied it (`clamp(a · opacity)`), then the migrated blur. */\nexport function applyOutputStage(img: { width: number; height: number; data: Uint8ClampedArray }, opacity: number, legacyOutputBlur?: number): void {\n if (opacity < 1) {\n const d = img.data;\n for (let i = 3; i < d.length; i += 4) d[i] = d[i] * opacity;\n }\n if (legacyOutputBlur && legacyOutputBlur > 0) applyBoxBlur(img, Math.min(Math.round(legacyOutputBlur), 40));\n}\n\nexport interface RouteInput {\n stack: LayerStack;\n region: CropBox;\n canvas: { width: number; height: number };\n transform: ImageTransform;\n /** Whether the source image is fully opaque. */\n sourceOpaque: boolean;\n}\n\n/**\n * Whether a document must render through the legacy base path to look exactly as before. Only\n * documents that still have the v1 / v2 shape qualify, and only where compositing in document\n * space cannot reproduce the old order of operations:\n *\n * - a rotation that is not a quarter turn, or a crop with fractional coordinates (the old path\n * resampled before adjusting);\n * - an enabled depth mask (applied after the blur and resampled with the canvas);\n * - output opacity below 1 (it rounded alpha before the depth mask);\n * - a Background blur filter with a crop or a 90° / 270° turn (the old blur clamped at the crop\n * edge and ran on the rotated buffer);\n * - a migrated output blur over a source with transparency (canvas premultiplication would round\n * colours before blurring);\n * - an adjustment-layer mask stored at another size than the canvas (it was resampled through\n * the projection on every render).\n */\nexport function needsLegacyBase(input: RouteInput): boolean {\n const { stack, region, canvas, transform } = input;\n if (!isLegacyShape(stack)) return false;\n const bg = stack.layers[0] as PixelLayer;\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) return true;\n if (bg.mask?.source?.enabled && bg.mask.source.maskSource) return true;\n if (stack.outputOpacity < 1) return true;\n const blur = blurFilter(bg);\n if (blur && blur.enabled && blur.radius > 0) {\n const full = region.x === 0 && region.y === 0 && region.width === canvas.width && region.height === canvas.height;\n const angle = normalizeAngle(transform.rotate);\n if (!full || angle === 90 || angle === 270) return true;\n }\n if ((stack.legacyOutputBlur ?? 0) > 0 && !input.sourceOpaque) return true;\n if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;\n return false;\n}\n\nconst isPlainAnnotation = (l: ImageLayer): l is AnnotationLayer =>\n l.type === 'annotation' && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask;\n\n/**\n * Splits the stack into what the compositor rasterizes and the annotation layers at the top that\n * draw as crisp vectors over the result (as annotations always did): the trailing run of plain\n * top-level annotation layers (hidden ones are skipped).\n */\nexport function splitOverlayAnnotations(layers: readonly ImageLayer[]): { composite: ImageLayer[]; overlay: AnnotationLayer[] } {\n let i = layers.length;\n while (i > 0) {\n const l = layers[i - 1];\n if (l.type === 'annotation' && (!l.visible || isPlainAnnotation(l))) i--;\n else break;\n }\n return {\n composite: layers.slice(0, i) as ImageLayer[],\n overlay: (layers.slice(i) as AnnotationLayer[]).filter((l) => l.visible),\n };\n}\n\n/** The integer rectangle covering `region`. */\nexport function coveringRect(region: CropBox): CropBox {\n const x = Math.floor(region.x);\n const y = Math.floor(region.y);\n return { x, y, width: Math.ceil(region.x + region.width) - x, height: Math.ceil(region.y + region.height) - y };\n}\n\n/**\n * The base image of a layered document without a canvas: the stack composited over `region`\n * (integer), projected under a quarter-turn `transform`, then output opacity and the migrated\n * blur. Annotation layers need a canvas and are skipped unless `sources` can rasterize them.\n * This is what the renderer's base canvas holds for such documents.\n */\nexport function renderLayeredBase(\n stack: LayerStack,\n region: CropBox,\n transform: ImageTransform,\n sources: CompositorSources,\n compositor = new LayerCompositor()\n): { width: number; height: number; data: Uint8ClampedArray } {\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) {\n throw new Error('renderLayeredBase needs an integer region and a quarter-turn rotation.');\n }\n const composite = compositor.composite(stack.layers, region, sources);\n const out = projectQuarterTurn(composite, region, region, transform);\n applyOutputStage(out, stack.outputOpacity, stack.legacyOutputBlur);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Histograms\n// ---------------------------------------------------------------------------\n\n/** An empty histogram. */\nexport function emptyHistogram(): ImageHistogram {\n return {\n red: new Uint32Array(HISTOGRAM_BINS),\n green: new Uint32Array(HISTOGRAM_BINS),\n blue: new Uint32Array(HISTOGRAM_BINS),\n luminance: new Uint32Array(HISTOGRAM_BINS),\n samples: 0,\n transparent: 0,\n shadows: [0, 0, 0],\n highlights: [0, 0, 0],\n max: 0,\n };\n}\n\n/** Adds the pixels of `rect` (in tile-local coordinates of a `stride`-wide RGBA buffer) to `h`. */\nexport function addToHistogram(h: ImageHistogram, data: ArrayLike<number>, stride: number, rect: CropBox): void {\n for (let y = rect.y; y < rect.y + rect.height; y++) {\n let i = (y * stride + rect.x) * 4;\n for (let x = 0; x < rect.width; x++, i += 4) {\n if (!(data[i + 3] > 0)) {\n h.transparent++;\n continue;\n }\n h.samples++;\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n h.red[r]++;\n h.green[g]++;\n h.blue[b]++;\n h.luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) h.shadows[0]++;\n if (g === 0) h.shadows[1]++;\n if (b === 0) h.shadows[2]++;\n if (r === 255) h.highlights[0]++;\n if (g === 255) h.highlights[1]++;\n if (b === 255) h.highlights[2]++;\n }\n }\n}\n\n/** Sums histograms (and recomputes `max`). */\nexport function sumHistograms(parts: Iterable<ImageHistogram>): ImageHistogram {\n const out = emptyHistogram();\n for (const p of parts) {\n for (let i = 0; i < HISTOGRAM_BINS; i++) {\n out.red[i] += p.red[i];\n out.green[i] += p.green[i];\n out.blue[i] += p.blue[i];\n out.luminance[i] += p.luminance[i];\n }\n out.samples += p.samples;\n out.transparent += p.transparent;\n for (let c = 0; c < 3; c++) {\n out.shadows[c] += p.shadows[c];\n out.highlights[c] += p.highlights[c];\n }\n }\n let max = 0;\n for (let i = 0; i < HISTOGRAM_BINS; i++) max = Math.max(max, out.red[i], out.green[i], out.blue[i], out.luminance[i]);\n out.max = max;\n return out;\n}\n","import type {\n Annotation,\n CropBox,\n ImagePoint,\n ExportOptions,\n ImageEditorFit,\n ImageEditorState,\n ImageTransform,\n PenAnnotation,\n RectAnnotation,\n CircleAnnotation,\n ArrowAnnotation,\n LineAnnotation,\n TextAnnotation,\n ImageAnnotation,\n} from './types';\nimport { applyAdjustments, applyDepthMask } from './filters';\nimport { adjustmentLayersKey } from './adjustment-layers';\nimport { canvasCompositeOperation, blendImageData, normalizeBlendMode } from './blend';\nimport { computeHistogram, type ImageHistogram } from './histogram';\nimport { normalizeProcessVersion } from './color';\nimport { buildLayerMaskPlanes } from './selection/layer-mask';\nimport { selectionOutline, quickMaskPixels } from './selection/overlay';\nimport type { SelectionMask } from './selection/mask';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport { flattenLayers, type ImageLayer, type PixelLayer } from './layers/model';\nimport {\n addToHistogram,\n applyOutputStage,\n baseProjection,\n coveringRect,\n emptyHistogram,\n needsLegacyBase,\n splitOverlayAnnotations,\n sumHistograms,\n} from './layers/render';\nimport { TILE, tileKey, type PixelSurface, type TileKey } from '../raster/surface';\nimport type { GlobalLight } from '../raster/effects/model';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { isLegacyShape as isLegacyStack } from './layers/migrate';\nimport {\n applyImageProjectionToContext,\n projectImagePoint,\n clampCropToImage,\n getAnnotationBounds,\n getRotatedSize,\n getViewportMetrics,\n getViewportProjection,\n normalizeAngle,\n type ImageProjection,\n type ViewportMetrics,\n} from './geometry';\n\nexport interface RenderOptions {\n viewportWidth?: number;\n viewportHeight?: number;\n showCropOverlay?: boolean;\n showSelectionOverlay?: boolean;\n interactive?: boolean;\n draftAnnotation?: Annotation | null;\n draftCrop?: CropBox | null;\n /** Viewport fit mode: 'contain' scales to full viewport bounds; 'margin' scales to 85% with 1.0 cap (default). */\n fit?: ImageEditorFit;\n /**\n * How the active selection is shown: marching ants (default when there is a selection),\n * a Quick Mask tint over the unselected area (default when `state.quickMask`), or not at all.\n */\n selectionDisplay?: 'ants' | 'quickMask' | 'none';\n /** Dash offset of the marching ants in screen pixels (animate it to make them march). */\n antsOffset?: number;\n /** A selection being drawn (e.g. a lasso), shown as a dashed outline in natural pixels. */\n draftSelectionPath?: ImagePoint[] | null;\n /** Document rectangles that are the only change since the last render (strokes); recomposites only there. */\n dirty?: CropBox[];\n /** The layer being edited: tiles keep a checkpoint below it so strokes and drags on it start there. */\n checkpointLayerId?: string | null;\n /** The paint tools' cursor: a circle of `diameter` px at (`x`, `y`) in natural pixels. */\n brushCursor?: { x: number; y: number; diameter: number } | null;\n /** Free Transform's frame, handles and reference point in natural pixels (`FreeTransformTool.overlay()`). */\n freeTransform?: {\n outline: ImagePoint[];\n handles: ImagePoint[];\n reference: ImagePoint;\n /** Warp mode: the mesh's grid lines as cubics, its anchors, and handles with their anchors. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n } | null;\n}\n\n/**\n * Image Cache to avoid refetching HTMLImageElement objects repeatedly during re-renders.\n */\nclass ImageCache {\n private cache = new Map<string, HTMLImageElement>();\n\n public async get(src: string): Promise<HTMLImageElement> {\n const existing = this.cache.get(src);\n if (existing && existing.complete) {\n return existing;\n }\n\n return new Promise((resolve, reject) => {\n if (typeof window === 'undefined' || typeof Image === 'undefined') {\n reject(new Error('Image loading is only supported in browser/DOM environments'));\n return;\n }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n this.cache.set(src, img);\n resolve(img);\n };\n img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));\n img.src = src;\n\n // In JSDOM or test environments where Image resource loading is inactive, resolve promptly\n if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {\n setTimeout(() => {\n try {\n Object.defineProperty(img, 'naturalWidth', { value: 800, configurable: true });\n Object.defineProperty(img, 'naturalHeight', { value: 600, configurable: true });\n Object.defineProperty(img, 'complete', { value: true, configurable: true });\n } catch {\n // Ignore definition errors in restrictive environments\n }\n this.cache.set(src, img);\n resolve(img);\n }, 10);\n }\n });\n }\n\n public set(src: string, img: HTMLImageElement): void {\n this.cache.set(src, img);\n }\n\n /** The loaded image for `src`, or `undefined` while it is not loaded yet. */\n public peek(src: string): HTMLImageElement | undefined {\n const img = this.cache.get(src);\n return img && img.complete ? img : undefined;\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\nexport const globalImageCache = new ImageCache();\n\n/**\n * Renders an image with all its transformations, filters, depth masks, and annotations\n * to a target HTMLCanvasElement.\n */\nexport class ImageEditorRenderer {\n public imageCache = globalImageCache;\n private cachedBaseCanvas: HTMLCanvasElement | null = null;\n private cachedBaseKey = '';\n /**\n * Histogram of the last viewport base render (after adjustments and layers, before\n * annotations); `null` until a render with pixel access has happened.\n */\n public previewHistogram: ImageHistogram | null = null;\n /** Called whenever the viewport base image is re-rendered with the new histogram. */\n public onPreviewHistogram?: (histogram: ImageHistogram | null) => void;\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 0;\n /**\n * The viewport's compositor. It only ever composites the viewport's stack, so its tile cache can\n * trust dirty rectangles; exports use `exportCompositor`.\n */\n public compositor = new LayerCompositor();\n private exportCompositor = new LayerCompositor({ maxCacheBytes: 64 * 1024 * 1024 });\n /** How the host resolves layer pixels (set by the controller). */\n public layerSources?: LayerSourcesHost;\n /**\n * Debug / test switch: `'legacy'` forces the old base path for documents that still have the\n * v1 / v2 shape, `'layers'` forces the compositor. Default: chosen per document.\n */\n public baseRoute: 'auto' | 'legacy' | 'layers' = 'auto';\n /** Which path the last base render took. */\n public lastBaseRoute: 'legacy' | 'layers' | null = null;\n private comp: CompositeCanvas | null = null;\n private depthMaps = new Map<string, { width: number; height: number; data: Uint8ClampedArray }>();\n private assetVersion = 0;\n private scratch: HTMLCanvasElement | null = null;\n private cachedPostCanvas: HTMLCanvasElement | null = null;\n private cachedPostKey = '';\n private cachedPostHistogram: ImageHistogram | null = null;\n\n /**\n * Helper to create a canvas element safely in browser/JSDOM.\n */\n public createCanvas(width: number, height: number): HTMLCanvasElement {\n if (typeof document !== 'undefined' && document.createElement) {\n const c = document.createElement('canvas');\n c.width = Math.max(1, Math.round(width));\n c.height = Math.max(1, Math.round(height));\n return c;\n }\n // Fallback for non-DOM / headless\n return {\n width: Math.max(1, Math.round(width)),\n height: Math.max(1, Math.round(height)),\n getContext: () => null,\n toDataURL: () => '',\n toBlob: () => {},\n } as unknown as HTMLCanvasElement;\n }\n\n /**\n * Renders the editor state onto a canvas (viewport or preview).\n */\n public async renderToCanvas(\n targetCanvas: HTMLCanvasElement,\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: RenderOptions = {}\n ): Promise<void> {\n const ctx = targetCanvas.getContext('2d');\n if (!ctx) return;\n\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const fit = options.fit ?? state.fit ?? 'margin';\n // Geometry helpers read `imageDimensions`; make sure they see the real source size so the\n // viewport drawn here and the hit-testing done by the UI share one projection.\n const geoState: ImageEditorState = {\n ...state,\n fit,\n imageDimensions:\n state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight\n ? state.imageDimensions\n : { width: naturalWidth, height: naturalHeight },\n };\n\n const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;\n\n // Viewport dimensions\n const vpWidth = options.viewportWidth || targetCanvas.width;\n const vpHeight = options.viewportHeight || targetCanvas.height;\n\n targetCanvas.width = vpWidth;\n targetCanvas.height = vpHeight;\n\n ctx.clearRect(0, 0, vpWidth, vpHeight);\n\n // If in compare mode, render original image directly and exit\n if (isOriginalCompared) {\n this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan, fit);\n return;\n }\n\n // When actively in crop tool or overlay requested, show full image so user can adjust the crop box\n const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === 'crop');\n const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);\n const { region } = projection;\n const rotated = getRotatedSize(region.width, region.height, transform);\n\n const route = this.chooseRoute(state, region, naturalWidth, naturalHeight);\n let overlay: Annotation[] = annotations;\n if (route === 'legacy') {\n // 1. Offscreen canvas for Base Image + Transform (Flip/Rotate) + Crop + Adjustments + Depth mask\n const baseKey = [\n this.getSourceId(img),\n state.sourceUrl,\n `${naturalWidth}x${naturalHeight}`,\n `${region.x},${region.y},${region.width},${region.height}`,\n `${projection.angle},${transform.flipH},${transform.flipV}`,\n `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,\n adjustmentLayersKey(adjustments.layers),\n `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`,\n ].join('|');\n\n let baseCanvas = this.cachedBaseCanvas;\n if (!baseCanvas || this.cachedBaseKey !== baseKey) {\n let histogram: ImageHistogram | null = null;\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {\n histogram = computeHistogram(pixels);\n });\n this.cachedBaseCanvas = baseCanvas;\n this.cachedBaseKey = baseKey;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n // 2. Draw the base canvas into the viewport (centred, fitted, zoomed, panned)\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n } else {\n // 1–2. The layer stack, composited per tile in document space, drawn through the projection.\n const split = splitOverlayAnnotations(state.layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const comp = this.updateComposite(split.composite, region, naturalWidth, naturalHeight, options);\n const base = this.layeredBase(comp, state, region, transform);\n if (base) {\n // Drawn exactly as the legacy base canvas is, so both paths look the same at any zoom.\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(base.canvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n this.publishHistogram(base.histogram ?? comp?.histogram() ?? null);\n }\n }\n\n // 3. Annotations live in natural image space: draw them through the same projection,\n // clipped to the visible (cropped) region.\n if (overlay.length > 0 || options.draftAnnotation) {\n ctx.save();\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {\n this.drawAnnotationSelection(ctx, ann);\n }\n }\n // A selected annotation drawn inside the composite still gets its handles.\n if (options.showSelectionOverlay && selectedAnnotationId && !overlay.some((a) => a.id === selectedAnnotationId)) {\n const sel = annotations.find((a) => a.id === selectedAnnotationId);\n if (sel) this.drawAnnotationSelection(ctx, sel);\n }\n\n // Render active live draft annotation\n if (options.draftAnnotation) {\n await this.drawAnnotation(ctx, options.draftAnnotation);\n }\n\n ctx.restore();\n }\n\n // 4. Selection: marching ants or Quick Mask, through the same projection.\n const display = options.selectionDisplay ?? (state.quickMask ? 'quickMask' : 'ants');\n if ((state.selection?.bounds && display !== 'none') || options.draftSelectionPath?.length) {\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (state.selection?.bounds && display === 'quickMask') this.drawQuickMask(ctx, state.selection);\n else if (state.selection?.bounds && display === 'ants') this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);\n if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);\n } catch {\n // Overlays are best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Brush cursor: the tip outline, one screen pixel wide, white over black.\n if (options.brushCursor) {\n const b = options.brushCursor;\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n const px = 1 / (projection.scale || 1);\n ctx.beginPath();\n ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);\n ctx.lineWidth = 2 * px;\n ctx.strokeStyle = 'rgba(0,0,0,0.6)';\n ctx.stroke();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Free Transform: the frame, square handles and the reference point, a fixed size on screen.\n if (options.freeTransform) {\n const t = options.freeTransform;\n ctx.save();\n try {\n const scr = (p: ImagePoint) => projectImagePoint(p, projection);\n ctx.beginPath();\n if (t.warp) {\n for (const c of t.warp.curves) {\n const [a, b, d, e] = c.map(scr);\n ctx.moveTo(a.x, a.y);\n ctx.bezierCurveTo(b.x, b.y, d.x, d.y, e.x, e.y);\n }\n for (const h of t.warp.handles) {\n const a = scr(h.anchor);\n const q = scr(h.point);\n ctx.moveTo(a.x, a.y);\n ctx.lineTo(q.x, q.y);\n }\n } else {\n const outline = t.outline.map(scr);\n outline.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));\n ctx.closePath();\n }\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n for (const h of [...t.handles, ...(t.warp?.anchors ?? [])].map(scr)) {\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#1e293b';\n ctx.fillRect(h.x - 4, h.y - 4, 8, 8);\n ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);\n }\n for (const h of (t.warp?.handles ?? []).map((w) => scr(w.point))) {\n ctx.beginPath();\n ctx.arc(h.x, h.y, 3, 0, Math.PI * 2);\n ctx.fillStyle = '#ffffff';\n ctx.fill();\n ctx.strokeStyle = '#1e293b';\n ctx.stroke();\n }\n const r = scr(t.reference);\n ctx.beginPath();\n ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);\n ctx.moveTo(r.x - 8, r.y);\n ctx.lineTo(r.x + 8, r.y);\n ctx.moveTo(r.x, r.y - 8);\n ctx.lineTo(r.x, r.y + 8);\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // 5. Draw Crop Grid Overlay if crop overlay is requested\n if (isActivelyCropping) {\n const activeCrop =\n options.draftCrop !== undefined && options.draftCrop !== null\n ? options.draftCrop\n : (crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight });\n this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));\n }\n }\n\n /**\n * Draws `region` of the source image with rotation/flip applied, then pixel adjustments\n * and the depth mask (which receives the identical transform so it stays aligned).\n */\n private async renderBaseImage(\n img: CanvasImageSource,\n naturalWidth: number,\n naturalHeight: number,\n region: CropBox,\n transform: ImageTransform,\n state: Pick<ImageEditorState, 'adjustments' | 'depthMask'>,\n onPixels?: (pixels: ImageData) => void\n ): Promise<HTMLCanvasElement> {\n const { adjustments, depthMask } = state;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const baseCanvas = this.createCanvas(baseW, baseH);\n const baseCtx = baseCanvas.getContext('2d');\n if (!baseCtx) return baseCanvas;\n\n const projection = getBaseProjection(region, transform, baseW, baseH);\n\n baseCtx.save();\n applyImageProjectionToContext(baseCtx, projection);\n baseCtx.drawImage(\n img,\n region.x, region.y, region.width, region.height,\n region.x, region.y, region.width, region.height\n );\n baseCtx.restore();\n\n const imageData = baseCtx.getImageData(0, 0, baseW, baseH);\n if (imageData) {\n // Layer masks live in natural pixels: resample them through the same projection.\n const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);\n applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});\n\n if (depthMask.enabled && depthMask.maskSource) {\n try {\n const maskImg = await this.imageCache.get(depthMask.maskSource);\n const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);\n const sx = maskSize.width / naturalWidth;\n const sy = maskSize.height / naturalHeight;\n const maskCanvas = this.createCanvas(baseW, baseH);\n const maskCtx = maskCanvas.getContext('2d');\n if (maskCtx) {\n // The depth map covers the whole source image; scale it to natural pixels and\n // apply the exact same rotate/flip/crop projection as the image itself.\n maskCtx.save();\n applyImageProjectionToContext(maskCtx, projection);\n maskCtx.drawImage(\n maskImg,\n region.x * sx, region.y * sy, region.width * sx, region.height * sy,\n region.x, region.y, region.width, region.height\n );\n maskCtx.restore();\n const maskData = maskCtx.getImageData(0, 0, baseW, baseH);\n if (maskData) {\n applyDepthMask(imageData, maskData, depthMask);\n }\n }\n } catch {\n // Mask failed to load; continue without crashing\n }\n }\n\n onPixels?.(imageData);\n baseCtx.putImageData(imageData, 0, 0);\n }\n\n return baseCanvas;\n }\n\n // --- Layered documents -------------------------------------------------------\n\n /** Which base path renders `state` (see `needsLegacyBase`). */\n private chooseRoute(state: ImageEditorState, region: CropBox, naturalWidth: number, naturalHeight: number): 'legacy' | 'layers' {\n let route: 'legacy' | 'layers';\n if (!state.layers?.length) route = 'legacy';\n else if (this.baseRoute === 'layers') route = 'layers';\n else {\n const stack = { layers: state.layers, outputOpacity: state.outputOpacity ?? 1, legacyOutputBlur: state.legacyOutputBlur };\n const shaped = this.baseRoute === 'legacy' && isLegacyStack(stack);\n route =\n shaped ||\n needsLegacyBase({\n stack,\n region,\n canvas: { width: naturalWidth, height: naturalHeight },\n transform: state.transform,\n sourceOpaque: this.layerSources?.sourceIsOpaque() ?? true,\n })\n ? 'legacy'\n : 'layers';\n }\n this.lastBaseRoute = route;\n return route;\n }\n\n /** Compositor sources for a `width × height` canvas: layer pixels, depth maps and annotation rasterizing. */\n public compositorSources(width: number, height: number): CompositorSources {\n const host = this.layerSources;\n return {\n width,\n height,\n surfaceOf: host ? (l: PixelLayer) => host.surfaceOf(l) : undefined,\n depthMap: (source) => {\n if (!source.enabled || !source.maskSource) return null;\n return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;\n },\n rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),\n version: `${host?.version() ?? ''}:${this.assetVersion}`,\n globalLight: host?.globalLight?.(),\n pattern: host?.pattern ? (id) => host.pattern!(id) : undefined,\n };\n }\n\n /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */\n /** Loads depth maps and annotation images the compositor reads synchronously. */\n public async prepareLayerAssets(layers: readonly ImageLayer[], width: number, height: number): Promise<void> {\n for (const l of flattenLayers(layers)) {\n const source = l.mask?.source;\n if (source?.enabled && source.maskSource) {\n const key = `${source.maskSource}|${width}x${height}`;\n if (this.depthMaps.has(key)) continue;\n try {\n const maskImg = await this.imageCache.get(source.maskSource);\n const canvas = this.createCanvas(width, height);\n const mctx = canvas.getContext('2d');\n const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;\n if (data) {\n this.depthMaps.set(key, { width, height, data: data.data });\n this.assetVersion++;\n }\n } catch {\n // A depth map that fails to load reveals the layer, as before.\n }\n }\n if (l.type === 'annotation' && l.annotation.type === 'image' && !this.imageCache.peek(l.annotation.src)) {\n try {\n await this.imageCache.get(l.annotation.src);\n this.assetVersion++;\n } catch {\n // Skipped, as a watermark that fails to load always was.\n }\n }\n }\n }\n\n /** Whether annotation layers can be rasterized here (a 2D canvas with pixel access). */\n public canRasterize(): boolean {\n const ctx = this.createCanvas(1, 1).getContext('2d');\n return Boolean(ctx && typeof ctx.getImageData === 'function');\n }\n\n /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */\n private rasterizeAnnotation(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null {\n if (!this.scratch) this.scratch = this.createCanvas(rect.width, rect.height);\n const canvas = this.scratch;\n if (canvas.width !== rect.width || canvas.height !== rect.height) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n try {\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n ctx.setTransform(1, 0, 0, 1, -rect.x, -rect.y);\n const img = annotation.type === 'image' ? this.imageCache.peek(annotation.src) ?? null : null;\n this.drawAnnotationShape(ctx, annotation, img);\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n return new Uint8ClampedArray(ctx.getImageData(0, 0, rect.width, rect.height).data);\n } catch {\n return null;\n }\n }\n\n private putTile(ctx: CanvasRenderingContext2D, data: Uint8ClampedArray, tx: number, ty: number, rect: CropBox): void {\n const ox = tx * TILE - rect.x;\n const oy = ty * TILE - rect.y;\n const x0 = Math.max(0, -ox);\n const y0 = Math.max(0, -oy);\n const x1 = Math.min(TILE, rect.width - ox);\n const y1 = Math.min(TILE, rect.height - oy);\n if (x1 <= x0 || y1 <= y0) return;\n const img = ctx.createImageData(TILE, TILE);\n img.data.set(data);\n ctx.putImageData(img, ox, oy, x0, y0, x1 - x0, y1 - y0);\n }\n\n /**\n * Brings the viewport's composite canvas (document space, covering `region`) up to date,\n * uploading only the tiles that changed since the last render.\n */\n private updateComposite(\n layers: readonly ImageLayer[],\n region: CropBox,\n width: number,\n height: number,\n options: Pick<RenderOptions, 'dirty' | 'checkpointLayerId'> = {}\n ): CompositeCanvas | null {\n const rect = coveringRect(region);\n let comp = this.comp;\n let fresh = false;\n if (!comp || comp.rect.x !== rect.x || comp.rect.y !== rect.y || comp.rect.width !== rect.width || comp.rect.height !== rect.height) {\n const canvas = this.createCanvas(rect.width, rect.height);\n comp = this.comp = new CompositeCanvas(canvas, rect);\n fresh = true;\n }\n const ctx = comp.canvas.getContext('2d');\n if (!ctx || typeof ctx.createImageData !== 'function') return null;\n let changed = false;\n this.compositor.compositeTiles(layers, rect, this.compositorSources(width, height), (tx, ty, data) => {\n const key = tileKey(tx, ty);\n if (comp!.tiles.get(key) === data) return;\n comp!.tiles.set(key, data);\n comp!.histograms.delete(key);\n this.putTile(ctx, data, tx, ty, rect);\n changed = true;\n }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? undefined : options.dirty });\n if (changed) comp.version++;\n comp.region = region;\n return comp;\n }\n\n /**\n * The base canvas of a layered document: the composite drawn through the base projection (crop,\n * rotation, flips at scale 1), then output opacity and the migrated blur, which run in the output\n * frame. Cached until the composite or those settings change.\n */\n private layeredBase(\n comp: CompositeCanvas | null,\n state: ImageEditorState,\n region: CropBox,\n transform: ImageTransform\n ): { canvas: HTMLCanvasElement; histogram: ImageHistogram | null } | null {\n if (!comp) return null;\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const key = `${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;\n if (this.cachedPostCanvas && this.cachedPostKey === key) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };\n const canvas =\n this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH\n ? this.cachedPostCanvas\n : this.createCanvas(baseW, baseH);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n ctx.clearRect(0, 0, baseW, baseH);\n ctx.save();\n applyImageProjectionToContext(ctx, baseProjection(region, transform, baseW, baseH));\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n ctx.drawImage(comp.canvas, comp.rect.x, comp.rect.y);\n ctx.restore();\n const histogram = this.applyPost(ctx, baseW, baseH, state);\n this.cachedPostCanvas = canvas;\n this.cachedPostKey = key;\n this.cachedPostHistogram = histogram;\n return { canvas, histogram };\n }\n\n /** Output opacity then the migrated blur, in place on a base canvas; returns its histogram. */\n private applyPost(ctx: CanvasRenderingContext2D, w: number, h: number, state: ImageEditorState): ImageHistogram | null {\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n if (!(opacity < 1) && !(blur > 0)) return null;\n const data = ctx.getImageData?.(0, 0, w, h);\n if (!data) return null;\n applyOutputStage(data, opacity, blur);\n ctx.putImageData(data, 0, 0);\n return computeHistogram(data);\n }\n\n private publishHistogram(histogram: ImageHistogram | null): void {\n if (histogram === this.previewHistogram) return;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n private quickMaskCache = new WeakMap<SelectionMask, HTMLCanvasElement>();\n private outlineCache = new WeakMap<SelectionMask, Float32Array>();\n\n /** Rubylith over the unselected area, drawn in natural image space. */\n private drawQuickMask(ctx: CanvasRenderingContext2D, selection: SelectionMask): void {\n let canvas = this.quickMaskCache.get(selection);\n if (!canvas) {\n canvas = this.createCanvas(selection.width, selection.height);\n const mctx = canvas.getContext('2d');\n if (!mctx || typeof mctx.createImageData !== 'function') return;\n const img = mctx.createImageData(selection.width, selection.height);\n img.data.set(quickMaskPixels(selection));\n mctx.putImageData(img, 0, 0);\n this.quickMaskCache.set(selection, canvas);\n }\n ctx.drawImage(canvas, 0, 0, selection.width, selection.height);\n }\n\n /** Marching ants: a white line under a black dashed one, one screen pixel wide. */\n private drawAnts(ctx: CanvasRenderingContext2D, selection: SelectionMask, scale: number, offset: number): void {\n let segs = this.outlineCache.get(selection);\n if (!segs) {\n segs = selectionOutline(selection);\n this.outlineCache.set(selection, segs);\n }\n if (!segs.length) return;\n const px = 1 / (scale || 1);\n ctx.beginPath();\n for (let i = 0; i < segs.length; i += 4) {\n ctx.moveTo(segs[i], segs[i + 1]);\n ctx.lineTo(segs[i + 2], segs[i + 3]);\n }\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') {\n ctx.setLineDash([4 * px, 4 * px]);\n ctx.lineDashOffset = -offset * px;\n }\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private drawDraftPath(ctx: CanvasRenderingContext2D, points: ImagePoint[], scale: number): void {\n const px = 1 / (scale || 1);\n ctx.beginPath();\n points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)));\n ctx.closePath();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([4 * px, 4 * px]);\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private getSourceId(img: CanvasImageSource): number {\n const key = img as unknown as object;\n let id = this.sourceIds.get(key);\n if (id === undefined) {\n id = ++this.nextSourceId;\n this.sourceIds.set(key, id);\n }\n return id;\n }\n\n /**\n * Draws an individual annotation onto the canvas.\n */\n public async drawAnnotation(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n const blend = normalizeBlendMode(ann.blend);\n if (blend === 'normal') {\n await this.drawAnnotationContent(ctx, ann);\n return;\n }\n const op = canvasCompositeOperation(blend);\n if (op) {\n ctx.save();\n ctx.globalCompositeOperation = op;\n await this.drawAnnotationContent(ctx, ann);\n ctx.restore();\n return;\n }\n if (!(await this.drawAnnotationBlended(ctx, ann))) {\n // No pixel access (e.g. a tainted or headless canvas): draw it normally.\n await this.drawAnnotationContent(ctx, ann);\n }\n }\n\n /**\n * Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /\n * vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):\n * draws the annotation on a same-size layer with the same transform, blends it into a copy of\n * the target's pixels with `blendImageData`, and copies the result back inside the current clip.\n */\n private async drawAnnotationBlended(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<boolean> {\n const canvas = ctx.canvas as HTMLCanvasElement | undefined;\n if (!canvas || typeof ctx.getTransform !== 'function' || typeof ctx.getImageData !== 'function') return false;\n const w = canvas.width;\n const h = canvas.height;\n const layer = this.createCanvas(w, h);\n const lctx = layer.getContext('2d');\n if (!lctx) return false;\n try {\n lctx.setTransform(ctx.getTransform());\n lctx.globalAlpha = ctx.globalAlpha;\n await this.drawAnnotationContent(lctx, ann);\n const backdrop = ctx.getImageData(0, 0, w, h);\n const source = lctx.getImageData(0, 0, w, h);\n if (!backdrop || !source) return false;\n blendImageData(backdrop, source, normalizeBlendMode(ann.blend));\n lctx.setTransform(1, 0, 0, 1, 0, 0);\n lctx.putImageData(backdrop, 0, 0);\n } catch {\n return false;\n }\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(layer, 0, 0);\n ctx.restore();\n return true;\n }\n\n private async drawAnnotationContent(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n let img: CanvasImageSource | null = null;\n if (ann.type === 'image') {\n try {\n img = await this.imageCache.get(ann.src);\n } catch {\n // Skip if watermark image failed to load\n }\n }\n this.drawAnnotationShape(ctx, ann, img);\n }\n\n /** Draws an annotation synchronously; an image annotation draws `img` (skipped when `null`). */\n public drawAnnotationShape(ctx: CanvasRenderingContext2D, ann: Annotation, img: CanvasImageSource | null): void {\n ctx.save();\n if (ann.opacity !== undefined) {\n ctx.globalAlpha = ann.opacity;\n }\n\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n const pen = ann as PenAnnotation;\n if (pen.points.length > 0) {\n ctx.beginPath();\n ctx.strokeStyle = pen.strokeColor;\n ctx.lineWidth = pen.strokeWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.moveTo(pen.points[0].x, pen.points[0].y);\n for (let i = 1; i < pen.points.length; i++) {\n ctx.lineTo(pen.points[i].x, pen.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n\n case 'rect': {\n const rect = ann as RectAnnotation;\n ctx.strokeStyle = rect.strokeColor;\n ctx.lineWidth = rect.strokeWidth;\n if (rect.fillColor) {\n ctx.fillStyle = rect.fillColor;\n ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n if (rect.strokeWidth > 0) {\n ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);\n }\n break;\n }\n\n case 'circle': {\n const circle = ann as CircleAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = circle.strokeColor;\n ctx.lineWidth = circle.strokeWidth;\n ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);\n if (circle.fillColor) {\n ctx.fillStyle = circle.fillColor;\n ctx.fill();\n }\n if (circle.strokeWidth > 0) {\n ctx.stroke();\n }\n break;\n }\n\n case 'arrow': {\n const arrow = ann as ArrowAnnotation;\n ctx.strokeStyle = arrow.strokeColor;\n ctx.fillStyle = arrow.strokeColor;\n ctx.lineWidth = arrow.strokeWidth;\n\n // Line body\n ctx.beginPath();\n ctx.moveTo(arrow.x, arrow.y);\n ctx.lineTo(arrow.endX, arrow.endY);\n ctx.stroke();\n\n // Arrow head\n const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);\n const headLen = Math.max(12, arrow.strokeWidth * 3);\n ctx.beginPath();\n ctx.moveTo(arrow.endX, arrow.endY);\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle - Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle - Math.PI / 6)\n );\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle + Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle + Math.PI / 6)\n );\n ctx.closePath();\n ctx.fill();\n break;\n }\n\n case 'line': {\n const line = ann as LineAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = line.strokeColor;\n ctx.lineWidth = line.strokeWidth;\n ctx.moveTo(line.x, line.y);\n ctx.lineTo(line.endX, line.endY);\n ctx.stroke();\n break;\n }\n\n case 'text': {\n const textAnn = ann as TextAnnotation;\n ctx.font = `${textAnn.fontWeight || 'normal'} ${textAnn.fontSize}px ${textAnn.fontFamily || 'sans-serif'}`;\n ctx.fillStyle = textAnn.color;\n ctx.textBaseline = 'top';\n ctx.fillText(textAnn.text, textAnn.x, textAnn.y);\n break;\n }\n\n case 'image': {\n const imgAnn = ann as ImageAnnotation;\n if (img) ctx.drawImage(img, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);\n break;\n }\n }\n\n ctx.restore();\n }\n\n /**\n * Draws selection box around active annotation.\n */\n private drawAnnotationSelection(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.strokeStyle = '#3b82f6'; // Tailwind blue-500\n ctx.lineWidth = 1.5;\n ctx.setLineDash([4, 4]);\n\n const bounds = getAnnotationBounds(ann);\n const pad = 4;\n ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);\n\n // Draw handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#3b82f6';\n ctx.setLineDash([]);\n const handleSize = 6;\n const corners = [\n { x: bounds.x - pad, y: bounds.y - pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y - pad },\n { x: bounds.x - pad, y: bounds.y + bounds.height + pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad },\n ];\n for (const c of corners) {\n ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */\n public getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n return getAnnotationBounds(ann);\n }\n\n /**\n * Draws a rule-of-thirds crop grid overlay. All rectangles come from\n * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.\n */\n private drawCropOverlay(ctx: CanvasRenderingContext2D, metrics: ViewportMetrics): void {\n const { imageRect, cropRect } = metrics;\n if (!cropRect) return;\n\n const imgL = imageRect.x;\n const imgT = imageRect.y;\n const imgR = imageRect.x + imageRect.width;\n const imgB = imageRect.y + imageRect.height;\n const startX = cropRect.x;\n const startY = cropRect.y;\n const cropBoxW = cropRect.width;\n const cropBoxH = cropRect.height;\n\n ctx.save();\n\n // Dark semi-transparent scrim outside crop box (within the image)\n ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n // Top\n ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));\n // Bottom\n ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));\n // Left\n ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);\n // Right\n ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);\n\n // Crop border\n ctx.strokeStyle = '#ffffff';\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);\n\n // Rule of thirds lines\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n // Vertical lines\n ctx.moveTo(startX + cropBoxW / 3, startY);\n ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);\n ctx.moveTo(startX + (cropBoxW * 2) / 3, startY);\n ctx.lineTo(startX + (cropBoxW * 2) / 3, startY + cropBoxH);\n // Horizontal lines\n ctx.moveTo(startX, startY + cropBoxH / 3);\n ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);\n ctx.moveTo(startX, startY + (cropBoxH * 2) / 3);\n ctx.lineTo(startX + cropBoxW, startY + (cropBoxH * 2) / 3);\n ctx.stroke();\n\n // Corner and edge handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#0284c7';\n ctx.lineWidth = 1.5;\n const handleSize = 10;\n const handles = [\n // Corners\n { x: startX, y: startY },\n { x: startX + cropBoxW, y: startY },\n { x: startX, y: startY + cropBoxH },\n { x: startX + cropBoxW, y: startY + cropBoxH },\n // Edges\n { x: startX + cropBoxW / 2, y: startY },\n { x: startX + cropBoxW / 2, y: startY + cropBoxH },\n { x: startX, y: startY + cropBoxH / 2 },\n { x: startX + cropBoxW, y: startY + cropBoxH / 2 },\n ];\n for (const h of handles) {\n ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n private drawFittedImage(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n imgW: number,\n imgH: number,\n vpW: number,\n vpH: number,\n zoom: number,\n pan: { x: number; y: number },\n fit: ImageEditorFit = 'margin'\n ): void {\n ctx.save();\n ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);\n ctx.scale(zoom, zoom);\n const fitScale =\n fit === 'contain'\n ? Math.min(vpW / imgW, vpH / imgH)\n : Math.min((vpW * 0.85) / imgW, (vpH * 0.85) / imgH, 1);\n const renderW = imgW * fitScale;\n const renderH = imgH * fitScale;\n ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);\n ctx.restore();\n }\n\n /**\n * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.\n */\n public async renderExport(\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: ExportOptions = {}\n ): Promise<HTMLCanvasElement> {\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const { crop, transform, annotations } = state;\n\n const region = crop\n ? clampCropToImage(crop, naturalWidth, naturalHeight)\n : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n\n const exportW = options.width || baseW;\n const exportH = options.height || baseH;\n\n const exportCanvas = this.createCanvas(exportW, exportH);\n const ctx = exportCanvas.getContext('2d');\n if (!ctx) return exportCanvas;\n\n let overlay: Annotation[] = annotations;\n let baseCanvas: HTMLCanvasElement;\n // A render of chosen layers (`layerIds`) always composites them; the legacy views describe the whole stack.\n if (!options.layerIds && this.chooseRoute(state, region, naturalWidth, naturalHeight) === 'legacy') {\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);\n } else {\n const layers = options.includeAnnotations === false ? removeAnnotationLayers(state.layers) : state.layers;\n const split = splitOverlayAnnotations(layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const rect = coveringRect(region);\n const comp = this.createCanvas(rect.width, rect.height);\n const cctx = comp.getContext('2d');\n if (cctx && typeof cctx.createImageData === 'function') {\n this.exportCompositor.compositeTiles(split.composite, rect, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {\n this.putTile(cctx, data, tx, ty, rect);\n });\n }\n baseCanvas = this.createCanvas(baseW, baseH);\n const bctx = baseCanvas.getContext('2d');\n if (bctx) {\n bctx.save();\n applyImageProjectionToContext(bctx, baseProjection(region, transform, baseW, baseH));\n bctx.beginPath();\n bctx.rect(region.x, region.y, region.width, region.height);\n bctx.clip();\n bctx.drawImage(comp, rect.x, rect.y);\n bctx.restore();\n this.applyPost(bctx, baseW, baseH, state);\n }\n }\n\n // Scale to target export dimensions\n ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);\n\n // Draw annotations (natural image space) through the same rotate/flip/crop projection\n if (options.includeAnnotations !== false && overlay.length > 0) {\n ctx.save();\n ctx.scale(exportW / baseW, exportH / baseH);\n applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n }\n ctx.restore();\n }\n\n return exportCanvas;\n }\n\n /**\n * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in\n * natural image pixel space of the current source image — the same space the crop box\n * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.\n */\n public async renderMask(\n state: ImageEditorState,\n options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }\n ): Promise<HTMLCanvasElement> {\n const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));\n const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));\n const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);\n const ctx = maskCanvas.getContext('2d');\n if (!ctx) return maskCanvas;\n\n // Background: unmasked (black)\n ctx.fillStyle = MASK_BLACK;\n ctx.fillRect(0, 0, naturalWidth, naturalHeight);\n\n const useCrop = options?.useCrop ?? true;\n const useAnnotations = options?.useAnnotations ?? true;\n\n // Crop box mask (if active)\n if (useCrop && state.crop) {\n ctx.fillStyle = MASK_WHITE;\n ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);\n }\n\n // Annotation masks (drawn in solid white, never in annotation colours)\n if (useAnnotations && state.annotations.length > 0) {\n for (const ann of state.annotations) {\n this.drawAnnotationMask(ctx, ann);\n }\n }\n\n // Anti-aliased edges produce grey pixels: threshold to pure black/white.\n const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);\n // The active selection (same natural space) is added to the mask.\n const selection =\n (options?.useSelection ?? true) &&\n state.selection?.bounds &&\n state.selection.width === naturalWidth &&\n state.selection.height === naturalHeight\n ? state.selection\n : null;\n if (imageData) {\n const d = imageData.data;\n for (let i = 0; i < d.length; i += 4) {\n const v = d[i] >= 128 || (selection !== null && selection.data[i >> 2] >= 128) ? 255 : 0;\n d[i] = v;\n d[i + 1] = v;\n d[i + 2] = v;\n d[i + 3] = 255;\n }\n ctx.putImageData(imageData, 0, 0);\n }\n\n return maskCanvas;\n }\n\n private drawAnnotationMask(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = MASK_WHITE;\n ctx.strokeStyle = MASK_WHITE;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n if (ann.points.length > 0) {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.points[0].x, ann.points[0].y);\n for (let i = 1; i < ann.points.length; i++) {\n ctx.lineTo(ann.points[i].x, ann.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n case 'circle': {\n ctx.beginPath();\n ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);\n ctx.fill();\n break;\n }\n case 'arrow':\n case 'line': {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.x, ann.y);\n ctx.lineTo(ann.endX, ann.endY);\n ctx.stroke();\n break;\n }\n case 'rect':\n case 'image':\n case 'text': {\n const b = getAnnotationBounds(ann);\n ctx.fillRect(b.x, b.y, b.width, b.height);\n break;\n }\n }\n ctx.restore();\n }\n}\n\nconst MASK_WHITE = '#ffffff';\nconst MASK_BLACK = '#000000';\n\nfunction getSourceSize(img: CanvasImageSource, fallbackW = 800, fallbackH = 600): { width: number; height: number } {\n const anyImg = img as any;\n return {\n width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,\n height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH,\n };\n}\n\nfunction getBaseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/** What the renderer asks its host (the controller) about layer pixels. */\nexport interface LayerSourcesHost {\n surfaceOf(layer: PixelLayer): PixelSurface;\n sourceIsOpaque(): boolean;\n version(): string;\n /** The document's global light for layer effects. */\n globalLight?(): GlobalLight | undefined;\n /** A document pattern for pattern effects (built-in patterns are found without it). */\n pattern?(id: string): EffectPattern | null;\n}\n\n/** The viewport's composite canvas and what each of its tiles holds. */\nclass CompositeCanvas {\n readonly tiles = new Map<TileKey, Uint8ClampedArray>();\n readonly histograms = new Map<TileKey, ImageHistogram>();\n version = 0;\n region: CropBox;\n constructor(readonly canvas: HTMLCanvasElement, readonly rect: CropBox) {\n this.region = rect;\n }\n\n /** Histogram of the composite over the region (per-tile parts are cached). */\n histogram(): ImageHistogram | null {\n if (!this.tiles.size) return null;\n const r = coveringRect(this.region);\n const parts: ImageHistogram[] = [];\n for (const [key, data] of this.tiles) {\n let h = this.histograms.get(key);\n if (!h) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const x0 = Math.max(r.x, tx * TILE);\n const y0 = Math.max(r.y, ty * TILE);\n const x1 = Math.min(r.x + r.width, tx * TILE + TILE);\n const y1 = Math.min(r.y + r.height, ty * TILE + TILE);\n h = emptyHistogram();\n if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });\n this.histograms.set(key, h);\n }\n parts.push(h);\n }\n return sumHistograms(parts);\n }\n}\n\nfunction removeAnnotationLayers(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: removeAnnotationLayers(l.children) } : l);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox, ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type SelectionMask } from './mask';\n\nexport interface ShapeSelectionOptions {\n /** Partial coverage along the edge (default true). */\n antiAlias?: boolean;\n}\n\ninterface Edge {\n top: number;\n bottom: number;\n ax: number;\n ay: number;\n bx: number;\n by: number;\n}\n\n/**\n * Even-odd polygon coverage over a `width × height` image: exact horizontal coverage on 4\n * sub-rows per pixel row (1 without anti-aliasing, where a pixel is in when its centre is).\n * Each sub-row only tests the edges that cross it (an active edge list over edges sorted by\n * their top), so the cost is about pixels + edges × rows they span.\n */\nexport function polygonCoverage(\n points: readonly ImagePoint[],\n width: number,\n height: number,\n antiAlias = true\n): Float32Array {\n const m = new Float32Array(width * height);\n if (points.length < 3 || width <= 0 || height <= 0) return m;\n const sub = antiAlias ? 4 : 1;\n const edges: Edge[] = [];\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;\n if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n }\n edges.sort((p, q) => p.top - q.top);\n let next = 0;\n let active: Edge[] = [];\n const xs: number[] = [];\n for (let py = 0; py < height; py++) {\n const row = py * width;\n for (let s = 0; s < sub; s++) {\n const y = py + (s + 0.5) / sub;\n while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);\n if (active.length) active = active.filter((e) => e.bottom > y);\n if (!active.length) continue;\n xs.length = 0;\n for (const e of active) {\n if (e.top <= y) xs.push(e.ax + ((y - e.ay) / (e.by - e.ay)) * (e.bx - e.ax));\n }\n xs.sort((a, b) => a - b);\n for (let k = 0; k + 1 < xs.length; k += 2) {\n const x0 = Math.max(0, xs[k]);\n const x1 = Math.min(width, xs[k + 1]);\n if (x1 <= x0) continue;\n const c0 = Math.floor(x0);\n const c1 = Math.min(width, Math.ceil(x1));\n for (let cx = c0; cx < c1; cx++) {\n let cov: number;\n if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);\n else {\n const c = cx + 0.5;\n cov = c >= x0 && c < x1 ? 1 : 0;\n }\n if (cov > 0) m[row + cx] += cov / sub;\n }\n }\n }\n }\n for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;\n return m;\n}\n\nfunction toMask(width: number, height: number, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */\nexport function polygonSelection(\n width: number,\n height: number,\n points: readonly ImagePoint[],\n options: ShapeSelectionOptions = {}\n): SelectionMask {\n return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));\n}\n\n/** Normalises a rectangle with a negative width or height. */\nexport function normalizeRect(rect: CropBox): CropBox {\n const x = rect.width < 0 ? rect.x + rect.width : rect.x;\n const y = rect.height < 0 ? rect.y + rect.height : rect.y;\n return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };\n}\n\n/** Rectangular marquee. Fractional edges get partial coverage when anti-aliased. */\nexport function rectSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n const r = normalizeRect(rect);\n const pts = [\n { x: r.x, y: r.y },\n { x: r.x + r.width, y: r.y },\n { x: r.x + r.width, y: r.y + r.height },\n { x: r.x, y: r.y + r.height },\n ];\n return polygonSelection(width, height, pts, options);\n}\n\n/** The ellipse inscribed in `rect` as a polygon fine enough that its chords stay within 0.05 px. */\nexport function ellipsePoints(rect: CropBox): ImagePoint[] {\n const r = normalizeRect(rect);\n const rx = r.width / 2;\n const ry = r.height / 2;\n const cx = r.x + rx;\n const cy = r.y + ry;\n const rmax = Math.max(rx, ry, 0.5);\n // Sagitta of a chord with angle step θ is r(1 − cos(θ/2)) ≈ rθ²/8 ≤ 0.05.\n const step = Math.sqrt(0.4 / rmax);\n const n = Math.min(4096, Math.max(16, Math.ceil((2 * Math.PI) / step)));\n const pts: ImagePoint[] = [];\n for (let i = 0; i < n; i++) {\n const t = (i / n) * 2 * Math.PI;\n pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });\n }\n return pts;\n}\n\n/** Elliptical marquee inscribed in `rect`. */\nexport function ellipseSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n return polygonSelection(width, height, ellipsePoints(rect), options);\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\n\n/** How the magic wand measures colour difference. */\nexport type ColorDistance = 'rgb' | 'perceptual';\n\nexport interface MagicWandOptions {\n /**\n * 0..255. `rgb`: the largest per-channel difference in levels (alpha included), as\n * Photoshop's wand. `perceptual`: CIE76 ΔE*ab, with 255 ≙ ΔE 100. Default 32.\n */\n tolerance?: number;\n /** Only the 4-connected region around the seed (default true); otherwise every similar pixel. */\n contiguous?: boolean;\n /** Soft one-pixel fringe on the edge (default true). */\n antiAlias?: boolean;\n /** Default `rgb`. */\n distance?: ColorDistance;\n}\n\nfunction checkPixels(p: PixelBuffer): number {\n const n = p.width * p.height;\n if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {\n throw new Error('Invalid pixel buffer size');\n }\n if (p.data.length < n * 4) throw new Error('Pixel data is shorter than width × height × 4');\n return n;\n}\n\nconst SRGB_TO_LINEAR = (() => {\n const t = new Float32Array(256);\n for (let i = 0; i < 256; i++) {\n const v = i / 255;\n t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n }\n return t;\n})();\n\nconst labF = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);\n\n/** CIE Lab (D65) of an 8-bit sRGB colour. */\nexport function srgb8ToLab(r: number, g: number, b: number): [number, number, number] {\n const rl = SRGB_TO_LINEAR[r & 255];\n const gl = SRGB_TO_LINEAR[g & 255];\n const bl = SRGB_TO_LINEAR[b & 255];\n const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;\n const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;\n const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;\n const fx = labF(x);\n const fy = labF(y);\n const fz = labF(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\n/** `similar(i)` for the wand: does pixel `i` match the seed colour within the tolerance? */\nfunction similarity(pixels: PixelBuffer, seed: number, tolerance: number, distance: ColorDistance): (i: number) => boolean {\n const d = pixels.data;\n const tr = d[seed * 4];\n const tg = d[seed * 4 + 1];\n const tb = d[seed * 4 + 2];\n const ta = d[seed * 4 + 3];\n const tol = Math.max(0, Math.floor(tolerance + 1e-3));\n if (distance === 'perceptual') {\n const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);\n const de = (Math.max(0, tolerance) * 100) / 255;\n const de2 = de * de + 1e-6;\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n // Copying a selection can leave RGB under zero alpha; hidden colours must not split a\n // transparent region.\n if (ta === 0 && a === 0) return true;\n if (Math.abs(a - ta) > tol) return false;\n const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);\n return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;\n };\n }\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n if (ta === 0 && a === 0) return true;\n return (\n Math.abs(d[p] - tr) <= tol &&\n Math.abs(d[p + 1] - tg) <= tol &&\n Math.abs(d[p + 2] - tb) <= tol &&\n Math.abs(a - ta) <= tol\n );\n };\n}\n\n/**\n * Softens hard edges: an edge pixel (one whose 3×3 neighbourhood differs from it) becomes\n * `½ + ½·avg` if selected, `½·avg` if not (neighbours outside the image count as the pixel).\n * Only the box `x0..x1 × y0..y1` (exclusive ends) is visited.\n */\nfunction antialiasMarks(marks: Uint8Array, w: number, h: number, x0: number, y0: number, x1: number, y1: number): Uint8Array {\n const out = marks.slice();\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = y * w + x;\n const v = marks[i];\n let sum = 0;\n let edge = false;\n for (let dy = -1; dy <= 1; dy++) {\n const ny = y + dy;\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx;\n const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];\n if (n !== v) edge = true;\n sum += n;\n }\n }\n if (edge) {\n const avg = sum / (9 * 255);\n out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);\n }\n }\n }\n return out;\n}\n\n/**\n * Magic wand: pixels similar to the seed pixel. Contiguous mode is a scanline span fill with an\n * explicit stack (fill a run, then scan the rows above and below it for new runs), so it is\n * linear in the pixels visited and never recurses. An empty selection when the seed is outside.\n */\nexport function magicWand(pixels: PixelBuffer, seed: ImagePoint, options: MagicWandOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h } = pixels;\n const sx = Math.floor(seed.x);\n const sy = Math.floor(seed.y);\n if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);\n const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? 'rgb');\n const marks = new Uint8Array(n);\n let bx0 = w;\n let by0 = h;\n let bx1 = -1;\n let by1 = -1;\n if (options.contiguous ?? true) {\n // Span fill. Each stack entry is a seed pixel index.\n const stack: number[] = [sy * w + sx];\n while (stack.length) {\n const idx = stack.pop()!;\n if (marks[idx]) continue;\n const y = (idx / w) | 0;\n const row = y * w;\n let l = idx - row;\n let r = l;\n while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;\n while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;\n marks.fill(255, row + l, row + r + 1);\n if (l < bx0) bx0 = l;\n if (r > bx1) bx1 = r;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n for (const ny of [y - 1, y + 1]) {\n if (ny < 0 || ny >= h) continue;\n const nrow = ny * w;\n let inRun = false;\n for (let nx = l; nx <= r; nx++) {\n const ok = !marks[nrow + nx] && similar(nrow + nx);\n if (ok && !inRun) stack.push(nrow + nx);\n inRun = ok;\n }\n }\n }\n } else {\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) {\n if (similar(row + x)) {\n marks[row + x] = 255;\n if (x < bx0) bx0 = x;\n if (x > bx1) bx1 = x;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n }\n }\n }\n }\n if (bx1 < 0) return createSelectionMask(w, h);\n if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);\n return createSelectionMask(\n w,\n h,\n antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))\n );\n}\n\n/** One Color Range sample: a colour (0..255 levels or `#rrggbb`) and, optionally, where it was picked. */\nexport interface ColorRangeSample {\n color: readonly [number, number, number] | string;\n at?: ImagePoint;\n}\n\nexport interface ColorRangeOptions {\n /** 0..200 levels (default 40): coverage falls off linearly to 0 at `fuzziness + 1` levels. */\n fuzziness?: number;\n /**\n * Localized Color Clusters: a sample with a position also fades with distance from it,\n * reaching 0 at this many pixels.\n */\n localized?: number;\n}\n\nfunction sampleColor(c: ColorRangeSample['color']): [number, number, number] {\n if (typeof c === 'string') {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) throw new Error(`Invalid colour \"${c}\"`);\n const v = parseInt(m[1], 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n }\n return [c[0], c[1], c[2]];\n}\n\n/**\n * Select › Color Range › Sampled Colors: each pixel takes its best match over the samples; a\n * match falls off linearly with the largest per-channel difference, `1 − d / (fuzziness + 1)`.\n * Transparent pixels are never selected.\n */\nexport function colorRange(pixels: PixelBuffer, samples: readonly ColorRangeSample[], options: ColorRangeOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h, data } = pixels;\n const f = Math.max(0, options.fuzziness ?? 40) + 1;\n const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));\n const radius = options.localized !== undefined ? Math.max(1, options.localized) : undefined;\n const out = new Uint8Array(n);\n if (!parsed.length) return createSelectionMask(w, h, out);\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (data[p + 3] <= 0) continue;\n const x = i % w;\n const y = (i - x) / w;\n let best = 0;\n for (const s of parsed) {\n const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));\n let k = Math.min(1, Math.max(0, 1 - d / f));\n if (k > 0 && radius !== undefined && s.at) {\n const dist = Math.hypot(x - s.at.x, y - s.at.y);\n k *= Math.min(1, Math.max(0, 1 - dist / radius));\n }\n if (k > best) best = k;\n }\n out[i] = coverageToByte(best);\n }\n return createSelectionMask(w, h, out);\n}\n\nexport type ToneRangePreset = 'shadows' | 'midtones' | 'highlights';\n\n/** Luma bounds (0..255, Rec. 601) of the tonal presets. */\nexport const TONE_RANGE_PRESETS: Record<ToneRangePreset, [number, number]> = {\n shadows: [0, 65],\n midtones: [105, 150],\n highlights: [190, 255],\n};\n\n/**\n * Select › Color Range › Shadows / Midtones / Highlights: pixels whose luma lies in `lo..hi` are\n * selected; outside, selection falls off linearly to 0 over `falloff` levels.\n */\nexport function toneRange(\n pixels: PixelBuffer,\n range: ToneRangePreset | readonly [number, number],\n falloff = 20\n): SelectionMask {\n const n = checkPixels(pixels);\n const [lo, hi] = typeof range === 'string' ? TONE_RANGE_PRESETS[range] : range;\n const ramp = Math.max(1e-3, falloff);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));\n const outside = l < lo ? lo - l : l > hi ? l - hi : 0;\n out[i] = coverageToByte(1 - outside / ramp);\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n\n/**\n * Select › Color Range › Reds … Magentas (`center` 0, 60, …, 300°): closeness of hue (full at\n * the centre, none 60° away) × chroma × (2 − max), so neutrals are never selected.\n */\nexport function hueRange(pixels: PixelBuffer, center: number): SelectionMask {\n const n = checkPixels(pixels);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const r = d[p] / 255;\n const g = d[p + 1] / 255;\n const b = d[p + 2] / 255;\n const mx = Math.max(r, g, b);\n const mn = Math.min(r, g, b);\n const chroma = mx - mn;\n if (chroma <= 0) continue;\n let hue: number;\n if (mx === r) hue = (((g - b) / chroma) % 6 + 6) % 6;\n else if (mx === g) hue = (b - r) / chroma + 2;\n else hue = (r - g) / chroma + 4;\n hue *= 60;\n let dh = (((hue - center) % 360) + 360) % 360;\n dh = Math.min(dh, 360 - dh);\n out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n","// Portions ported from photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\nimport { distanceTransform, gaussianBlur, greyMorphology, shiftCoverage } from './modify';\n\n/**\n * Refine Edge / Select and Mask parameters (Photoshop's units). Refinement is edge-aware: within\n * `radius` pixels of the mask's edge the coverage is re-estimated from the image with a colour\n * guided filter (He et al.), so a coarse or misaligned mask (e.g. an AI segmentation) follows\n * the real edges of the image.\n */\nexport interface RefineEdgeParams {\n /** Edge-detection band radius in pixels (0 = no edge detection). */\n radius?: number;\n /** Narrow the band where the image edge is sharp (hair stays soft, hard edges stay hard). */\n smartRadius?: boolean;\n /** 0..100: rounds jagged edges. */\n smooth?: number;\n /** Pixels of Gaussian softening. */\n feather?: number;\n /** 0..100: hardens soft edges. */\n contrast?: number;\n /** −100..100: moves the edge out (positive) or in; 100 % is `max(radius, 4)` pixels. */\n shiftEdge?: number;\n}\n\nexport const DEFAULT_REFINE_EDGE_PARAMS: Required<RefineEdgeParams> = {\n radius: 0,\n smartRadius: false,\n smooth: 0,\n feather: 0,\n contrast: 0,\n shiftEdge: 0,\n};\n\n/** Guided-filter regulariser (colour guide, samples in 0..1). */\nexport const GUIDED_FILTER_EPS = 1e-4;\n\nconst fin = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\n/** Clamped, complete parameters. */\nexport function normalizeRefineEdgeParams(p: RefineEdgeParams = {}): Required<RefineEdgeParams> {\n return {\n radius: Math.min(250, Math.max(0, fin(p.radius, 0))),\n smartRadius: p.smartRadius === true,\n smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),\n feather: Math.min(1000, Math.max(0, fin(p.feather, 0))),\n contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),\n shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0))),\n };\n}\n\nconst gfRadius = (p: Required<RefineEdgeParams>) => Math.min(64, Math.max(1, Math.ceil(p.radius)));\nconst smoothSigma = (p: Required<RefineEdgeParams>) => (p.smooth / 100) * 4;\nconst featherSigma = (p: Required<RefineEdgeParams>) => p.feather / 2;\nfunction shiftAmount(p: Required<RefineEdgeParams>): number {\n const px = (p.shiftEdge / 100) * Math.max(p.radius, 4);\n return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;\n}\n\n/** Pixels around the mask's content that refinement can read or write. */\nexport function refineEdgeHalo(params: RefineEdgeParams = {}): number {\n const p = normalizeRefineEdgeParams(params);\n const g = gfRadius(p);\n const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;\n return (\n edge +\n Math.ceil(3 * smoothSigma(p)) +\n Math.ceil(3 * featherSigma(p)) +\n Math.ceil(Math.abs(shiftAmount(p))) +\n 2\n );\n}\n\n/** Box mean of radius `r` (window clipped to the image, divided by its pixel count). O(1) per pixel. */\nexport function boxMean(src: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const tmp = new Float32Array(w * h);\n const pre = new Float64Array(Math.max(w, h) + 1);\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];\n for (let x = 0; x < w; x++) {\n const lo = Math.max(0, x - r);\n const hi = Math.min(w, x + r + 1);\n tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n const out = new Float32Array(w * h);\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];\n for (let y = 0; y < h; y++) {\n const lo = Math.max(0, y - r);\n const hi = Math.min(h, y + r + 1);\n out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n return out;\n}\n\n/**\n * Guided filter with a colour guide (He et al., Eq. 19–21: `a_k = (Σ_k + εU)⁻¹ cov_k(I, p)`):\n * filters `p` so that it is locally a linear function of the guide's colour, which makes the\n * output's edges follow the guide's edges. `guide` is three planes of 0..1 values.\n */\nexport function guidedFilterColor(\n guide: readonly [Float32Array, Float32Array, Float32Array],\n p: Float32Array,\n w: number,\n h: number,\n r: number,\n eps = GUIDED_FILTER_EPS\n): Float32Array {\n const n = w * h;\n const [c0, c1, c2] = guide;\n const prod = (a: ArrayLike<number>, b: ArrayLike<number>) => {\n const o = new Float32Array(n);\n for (let i = 0; i < n; i++) o[i] = a[i] * b[i];\n return o;\n };\n const m0 = boxMean(c0, w, h, r);\n const m1 = boxMean(c1, w, h, r);\n const m2 = boxMean(c2, w, h, r);\n const mp = boxMean(p, w, h, r);\n const mip0 = boxMean(prod(c0, p), w, h, r);\n const mip1 = boxMean(prod(c1, p), w, h, r);\n const mip2 = boxMean(prod(c2, p), w, h, r);\n const s00 = boxMean(prod(c0, c0), w, h, r);\n const s01 = boxMean(prod(c0, c1), w, h, r);\n const s02 = boxMean(prod(c0, c2), w, h, r);\n const s11 = boxMean(prod(c1, c1), w, h, r);\n const s12 = boxMean(prod(c1, c2), w, h, r);\n const s22 = boxMean(prod(c2, c2), w, h, r);\n const a0 = new Float32Array(n);\n const a1 = new Float32Array(n);\n const a2 = new Float32Array(n);\n const b = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];\n const rr = s00[i] - u0 * u0 + eps;\n const rg = s01[i] - u0 * u1;\n const rb = s02[i] - u0 * u2;\n const gg = s11[i] - u1 * u1 + eps;\n const gb = s12[i] - u1 * u2;\n const bb = s22[i] - u2 * u2 + eps;\n const v0 = mip0[i] - u0 * up;\n const v1 = mip1[i] - u1 * up;\n const v2 = mip2[i] - u2 * up;\n // Inverse of the symmetric 3×3 matrix.\n const i00 = gg * bb - gb * gb;\n const i01 = gb * rb - rg * bb;\n const i02 = rg * gb - gg * rb;\n const i11 = rr * bb - rb * rb;\n const i12 = rb * rg - rr * gb;\n const i22 = rr * gg - rg * rg;\n const det = rr * i00 + rg * i01 + rb * i02;\n let x0 = 0, x1 = 0, x2 = 0;\n if (Math.abs(det) >= 1e-30) {\n x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;\n x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;\n x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;\n }\n a0[i] = x0;\n a1[i] = x1;\n a2[i] = x2;\n b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;\n }\n const ma0 = boxMean(a0, w, h, r);\n const ma1 = boxMean(a1, w, h, r);\n const ma2 = boxMean(a2, w, h, r);\n const mb = boxMean(b, w, h, r);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];\n return out;\n}\n\n/**\n * Per-pixel transition width (pixels) of the guide's luminance: local range over local maximum\n * gradient in a window of radius `r` (a sharp step gives about 2, a ramp of width L about L).\n */\nexport function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array {\n const n = w * h;\n const y = new Float32Array(n);\n for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];\n const g = new Float32Array(n);\n for (let yy = 0; yy < h; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;\n const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;\n g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);\n }\n }\n const gm = greyMorphology(g, w, h, r, true);\n const hi = greyMorphology(y, w, h, r, true);\n const lo = greyMorphology(y, w, h, r, false);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);\n return out;\n}\n\n/**\n * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when\n * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,\n * contrast and shift edge, in that order.\n */\nexport function refineCoverage(\n guide: readonly [Float32Array, Float32Array, Float32Array] | null,\n m: Float32Array,\n w: number,\n h: number,\n params: RefineEdgeParams = {}\n): Float32Array {\n const p = normalizeRefineEdgeParams(params);\n let a: Float32Array = Float32Array.from(m);\n if (p.radius > 0 && guide) {\n const n = w * h;\n const boundary = new Uint8Array(n);\n let any = false;\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = y * w + x;\n const v = m[i] >= 0.5;\n const diff =\n (x > 0 && m[i - 1] >= 0.5 !== v) ||\n (x + 1 < w && m[i + 1] >= 0.5 !== v) ||\n (y > 0 && m[i - w] >= 0.5 !== v) ||\n (y + 1 < h && m[i + w] >= 0.5 !== v);\n if (diff) {\n boundary[i] = 1;\n any = true;\n }\n }\n }\n if (any) {\n const dist = distanceTransform(boundary, w, h);\n const r = gfRadius(p);\n const q = guidedFilterColor(guide, m, w, h, r);\n const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;\n for (let i = 0; i < n; i++) {\n const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;\n const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));\n if (wgt > 0) {\n const qi = Math.min(1, Math.max(0, q[i]));\n a[i] = m[i] + (qi - m[i]) * wgt;\n }\n }\n }\n }\n const ss = smoothSigma(p);\n if (ss >= 0.3) {\n a = gaussianBlur(a, w, h, ss, 'clamp');\n const gain = 1 + ss;\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n a = gaussianBlur(a, w, h, featherSigma(p), 'clamp');\n if (p.contrast > 0) {\n const gain = 1 / (1 - (0.99 * p.contrast) / 100);\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n const amount = shiftAmount(p);\n if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);\n return a;\n}\n\n/** Three 0..1 planes of a window of an RGBA buffer. */\nexport function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array] {\n const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };\n const n = box.width * box.height;\n const r = new Float32Array(n);\n const g = new Float32Array(n);\n const b = new Float32Array(n);\n const d = pixels.data;\n for (let y = 0; y < box.height; y++) {\n for (let x = 0; x < box.width; x++) {\n const i = y * box.width + x;\n const p = ((box.y + y) * pixels.width + box.x + x) * 4;\n r[i] = d[p] / 255;\n g[i] = d[p + 1] / 255;\n b[i] = d[p + 2] / 255;\n }\n }\n return [r, g, b];\n}\n\n/**\n * Refine Edge on a selection (or a coarse AI mask loaded as one) against `pixels` (the image,\n * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small\n * selection on a large image stays cheap.\n */\nexport function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params: RefineEdgeParams = {}): SelectionMask {\n const p = normalizeRefineEdgeParams(params);\n if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {\n throw new Error('Refine Edge needs pixels the size of the selection');\n }\n if (!mask.bounds) return mask;\n const halo = refineEdgeHalo(p);\n // The guided filter reads 2r beyond the band; the window must hold that too.\n const x0 = Math.max(0, mask.bounds.x - halo);\n const y0 = Math.max(0, mask.bounds.y - halo);\n const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);\n const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);\n const ww = x1 - x0;\n const wh = y1 - y0;\n const m = new Float32Array(ww * wh);\n for (let y = 0; y < wh; y++) {\n const src = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;\n }\n const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;\n const refined = refineCoverage(guide, m, ww, wh, p);\n const out = new Uint8Array(mask.width * mask.height);\n for (let y = 0; y < wh; y++) {\n const dst = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);\n }\n return createSelectionMask(mask.width, mask.height, out);\n}\n","// Portions ported from photocraft/crates/engine/src/transform_cmds.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Free Transform sessions: what a transform moves (a layer's pixels with its linked mask, a lone\n * unlinked mask, the selected pixels as a floating piece, a copy of them on a new layer, or just\n * the selection outline), a live preview while the geometry changes, and one undo step when it is\n * committed with full-quality resampling.\n */\n\nimport {\n TiledSurface,\n contentBounds,\n readRegion,\n surfaceFromPixels,\n writeRegion,\n type RasterRect,\n} from '../raster/surface';\nimport {\n compositeSurfaceOver,\n reduceSurface,\n transformSurface,\n TransformTooLargeError,\n warpSurface,\n isInterpolation,\n type Interpolation,\n type ResampleOptions,\n} from '../raster/transform';\nimport { multiplyHomographies, type Homography } from '../geom/homography';\nimport { findLayer, insertLayer, mapLayer, newLayerId, type ImageLayer, type LayerTarget, type PixelLayer, type RasterMask, type SmartObjectLayer } from './layers/model';\nimport { LayerOperationError, requireLayer, requireUnlocked } from './layers/ops';\nimport { contentsBox, isContentsDecoded, placementGeometry, renderSmartObject, setSmartPlacement } from './layers/smart';\nimport { createSelectionMask, type SelectionMask } from './selection/mask';\nimport {\n geometryHomography,\n hasWarp,\n identityTransform,\n isIdentityTransform,\n transformMap,\n type FreeTransformGeometry,\n} from './transform';\nimport type { CropBox } from './types';\n\n/**\n * What a Free Transform moves:\n * - `layer`: the layer's pixels with its linked mask (or, with `channel: 'mask'` on an unlinked\n * mask, the mask alone);\n * - `selection`: the selected pixels (and the same part of a linked mask), lifted and committed\n * back into the layer; the selection outline moves with them;\n * - `selection-copy`: Layer via Copy, then the copy is transformed on a new layer above;\n * - `selection-outline`: Transform Selection, only the outline;\n * - `auto` (default): `selection` when something is selected, else `layer`.\n */\nexport type FreeTransformContent = 'auto' | 'layer' | 'selection' | 'selection-copy' | 'selection-outline';\n\nexport const FREE_TRANSFORM_CONTENTS: readonly FreeTransformContent[] = ['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'];\n\nexport interface FreeTransformInit {\n /** The layer (default the active one); not needed for `selection-outline`. */\n layerId?: string;\n content?: FreeTransformContent;\n /**\n * `pixels` (default) moves the pixels with a linked mask; `mask` moves an unlinked mask alone (a\n * linked one moves with its pixels). Defaults to the active target on the active layer.\n */\n channel?: LayerTarget;\n /** Resampling of the committed result (default bicubic). */\n interpolation?: Interpolation;\n}\n\n/** How a live preview is drawn (the UI knows these, the controller doesn't). */\nexport interface FreeTransformPreview {\n /** Screen pixels per document pixel: below 1 the preview samples a reduced copy in blocks. */\n scale?: number;\n /** Only this part of the document (the visible area) is resampled. */\n clip?: CropBox;\n}\n\n/** What a committed transform changed. */\nexport interface FreeTransformResult {\n /** The layer that holds the result (the new layer for `selection-copy`; `null` for `selection-outline`). */\n layerId: string | null;\n content: Exclude<FreeTransformContent, 'auto'>;\n /** What moved. */\n moved: Array<'pixels' | 'mask' | 'selection'>;\n /** The frame the transform started from (document pixels). */\n box: CropBox;\n /** Where the frame's corners went. */\n corners: FreeTransformGeometry['corners'];\n /** Document bounds of the moved content after the transform (pixels, or the mask or the outline). */\n bounds: CropBox | null;\n /** False when the geometry was the identity and nothing was recorded. */\n changed: boolean;\n}\n\n/** @internal What the session needs from the controller. */\nexport interface FreeTransformHost {\n layers(): ImageLayer[];\n activeLayerId(): string | null;\n activeTarget(): LayerTarget;\n selection(): SelectionMask | null;\n canvas(): { width: number; height: number };\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n pixelsOf(layer: PixelLayer | SmartObjectLayer): TiledSurface;\n /** Opaque pre-transform snapshot. */\n capture(): unknown;\n /** Shows a live state (no history). */\n show(layers: ImageLayer[], selection: SelectionMask | null, activeLayerId: string | null): void;\n /** Records `before` as the undo step of the state now shown. */\n record(before: unknown): void;\n /** Puts `before` back (no history). */\n restore(before: unknown): void;\n}\n\n/** A surface that moves: in layer space (`offset` to document), composited back or replacing. */\ninterface Part {\n what: 'pixels' | 'mask' | 'selection';\n offset: { x: number; y: number };\n /** RGBA (pixels, or a mask part as grey + alpha) or one channel (a whole mask, the outline). */\n moving: TiledSurface;\n /** What stays: composited under the moved part (`null`: the moved part replaces the surface). */\n base: TiledSurface | null;\n /** Reduced copies for previews, by factor. */\n reduced: Map<number, TiledSurface>;\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\nconst T = (x: number, y: number): Homography => [1, 0, x, 0, 1, y, 0, 0, 1];\n\n/** Preview block size for a screen scale: 1 at 100 % and above, then powers of two up to 8. */\nexport function previewStep(scale: number | undefined): number {\n if (!scale || !(scale > 0) || scale >= 1) return 1;\n return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));\n}\n\nfunction toDoc(r: RasterRect | null, o: { x: number; y: number }): CropBox | null {\n return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;\n}\n\nfunction selectionSurface(sel: SelectionMask): TiledSurface {\n return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);\n}\n\n/**\n * Lifts the selected part of an RGBA surface (layer space): the lifted pixels (alpha × coverage)\n * and the rest (alpha × (1 − coverage); fully lifted pixels become transparent black).\n */\nfunction liftPixels(surface: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, area);\n const kept = new Uint8ClampedArray(px);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const i = (y * area.width + x) * 4;\n if (k <= 0) {\n px[i] = px[i + 1] = px[i + 2] = px[i + 3] = 0;\n continue;\n }\n px[i + 3] = px[i + 3] * k;\n if (!px[i + 3]) px[i] = px[i + 1] = px[i + 2] = 0;\n kept[i + 3] = kept[i + 3] * (1 - k);\n if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };\n}\n\n/**\n * Lifts the selected part of a mask (layer space): the values as grey + alpha (alpha = coverage)\n * and the rest (the selected values replaced by the default). Everything selected moves,\n * default-valued parts included, so the mask stays aligned with the moved pixels.\n */\nfunction liftMask(mask: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const v = readRegion(mask, area);\n const def = mask.defaultValue;\n const lifted = new Uint8ClampedArray(area.width * area.height * 4);\n const rest = new Uint8ClampedArray(v.length);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const i = y * area.width + x;\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const g = v[i];\n if (k > 0) {\n lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;\n lifted[i * 4 + 3] = k * 255;\n }\n rest[i] = g * (1 - k) + def * k;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };\n}\n\n/**\n * A Free Transform in progress (see `ImageEditorController.beginFreeTransform`). Change the\n * geometry with `update` (each change shows a live preview without history), then `commit()`\n * (one undo step, resampled at full quality) or `cancel()` (everything back as it was).\n */\nexport class FreeTransformSession {\n readonly content: Exclude<FreeTransformContent, 'auto'>;\n /** The layer being transformed (`null` for `selection-outline`). */\n readonly layerId: string | null;\n /** The new layer of a `selection-copy`. */\n readonly copyId: string | null = null;\n /** The frame (document pixels). */\n readonly box: CropBox;\n /** What moves. */\n readonly moves: ReadonlyArray<'pixels' | 'mask' | 'selection'>;\n /**\n * The geometry the transform starts from: the identity over `box`, or for a smart object its\n * placement (`box` is then the contents' rectangle in contents pixels, and the transform edits\n * the placement without resampling anything twice).\n */\n readonly startGeometry: FreeTransformGeometry;\n /** True when the layer is a smart object (the transform sets its placement). */\n readonly smartObject: boolean = false;\n private geometryValue: FreeTransformGeometry;\n private readonly smart: SmartObjectLayer | null = null;\n private readonly smartReduced = new Map<number, TiledSurface>();\n private readonly startInterp: Interpolation;\n private smartMasksMoved = false;\n private interp: Interpolation;\n private readonly parts: Part[] = [];\n private readonly before: unknown;\n private readonly startLayers: ImageLayer[];\n private readonly startSelection: SelectionMask | null;\n private readonly startActive: string | null;\n /** The stack this session last showed: anything else means the document changed under it. */\n private shown: ImageLayer[];\n private closed = false;\n\n constructor(\n private readonly host: FreeTransformHost,\n init: FreeTransformInit = {}\n ) {\n if (init.interpolation !== undefined && !isInterpolation(init.interpolation)) fail(`Unknown interpolation \"${String(init.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = init.interpolation ?? 'bicubic';\n const sel = host.selection();\n const selected = Boolean(sel?.bounds);\n let content = init.content ?? 'auto';\n if (!(['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'] as const).includes(content)) {\n fail(`Unknown content \"${String(content)}\"; use auto, layer, selection, selection-copy or selection-outline.`);\n }\n const targetId = init.layerId ?? host.activeLayerId();\n const target = targetId && content !== 'selection-outline' ? findLayer(host.layers(), targetId)?.layer : undefined;\n const smartTarget = target?.type === 'smart' && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : 'pixels')) !== 'mask';\n // A smart object transforms as a whole, selection or not (as in Photoshop).\n if (content === 'auto') content = selected && !smartTarget ? 'selection' : 'layer';\n this.content = content;\n if (content !== 'layer' && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);\n const layers = host.layers();\n this.startLayers = layers;\n this.startSelection = sel;\n this.startActive = host.activeLayerId();\n this.before = host.capture();\n const moves: Array<'pixels' | 'mask' | 'selection'> = [];\n let box: CropBox | null = null;\n\n if (content === 'selection-outline') {\n this.layerId = null;\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n box = { ...sel!.bounds! };\n } else {\n const id = init.layerId ?? host.activeLayerId();\n if (!id) fail('No layer given and none is active; pass layerId (see list_layers).');\n const layer = requireLayer(layers, id!).layer;\n this.layerId = layer.id;\n const mask = layer.mask && !layer.mask.source ? layer.mask : undefined;\n const channel: LayerTarget = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : 'pixels');\n if (channel !== 'pixels' && channel !== 'mask') fail(`Unknown channel \"${String(channel)}\"; use pixels or mask.`);\n const linked = mask?.linked !== false;\n const maskAlone = channel === 'mask' && (!linked || layer.type !== 'pixel');\n if (channel === 'mask' && !mask) fail(layer.mask?.source ? `Layer \"${layer.name || id}\" has a depth mask, which is generated, not transformed.` : `Layer \"${layer.name || id}\" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);\n if (!maskAlone && layer.type !== 'pixel' && layer.type !== 'smart') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);\n if (!layer.visible && content !== 'selection-copy') fail(`Layer \"${layer.name || id}\" is hidden; show it before you transform it.`);\n if (content !== 'selection-copy') {\n requireUnlocked(layer, 'position', 'transform it');\n if (!maskAlone) requireUnlocked(layer, 'pixels', 'transform it');\n }\n const o = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const pixels = layer.type === 'pixel' ? host.pixelsOf(layer) : null;\n if (layer.type === 'smart' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n if (!isContentsDecoded(layer.contents)) fail(`The contents of \"${layer.name || id}\" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);\n this.smart = layer;\n this.smartObject = true;\n this.startGeometry = placementGeometry(layer);\n if (init.interpolation === undefined && layer.placement.interpolation) this.interp = layer.placement.interpolation;\n this.startInterp = this.interp;\n this.box = contentsBox(layer.contents);\n this.moves = mask && linked ? ['pixels', 'mask'] : ['pixels'];\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n return;\n }\n if (maskAlone) {\n if (content === 'selection-copy') fail('Layer via Copy copies pixels; transform a mask with content layer or selection.');\n if (content === 'selection') {\n const { lifted, rest } = liftMask(mask!.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: lifted, base: rest, reduced: new Map() });\n box = toDoc(contentBounds(lifted), o);\n } else {\n this.parts.push({ what: 'mask', offset: o, moving: mask!.surface, base: null, reduced: new Map() });\n box = toDoc(contentBounds(mask!.surface), o);\n }\n moves.push('mask');\n } else if (content === 'layer') {\n this.parts.push({ what: 'pixels', offset: o, moving: pixels!, base: null, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(pixels!), o);\n if (mask && linked) {\n this.parts.push({ what: 'mask', offset: o, moving: mask.surface, base: null, reduced: new Map() });\n moves.push('mask');\n if (!box) box = toDoc(contentBounds(mask.surface), o);\n }\n } else if (content === 'selection') {\n const { lifted, rest } = liftPixels(pixels!, sel!, o);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: rest, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(lifted), o);\n if (mask && linked) {\n const m = liftMask(mask.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: m.lifted, base: m.rest, reduced: new Map() });\n moves.push('mask');\n }\n } else {\n // selection-copy: the copy goes on a new layer above, with the source layer's offset.\n const { lifted } = liftPixels(pixels!, sel!, o);\n box = toDoc(contentBounds(lifted), o);\n if (box) {\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: `${layer.name || 'Layer'} copy`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: lifted,\n offset: { ...o },\n };\n (this as { copyId: string | null }).copyId = copy.id;\n const loc = findLayer(layers, layer.id)!;\n this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: null, reduced: new Map() });\n moves.push('pixels');\n }\n }\n if (content !== 'layer') {\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n }\n }\n if (!box && moves[0] === 'mask') fail('The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).');\n if (!box) fail(content === 'layer' ? 'The layer has no pixels to transform.' : 'The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).');\n this.box = box!;\n this.moves = moves;\n this.startGeometry = identityTransform(box!);\n this.startInterp = this.interp;\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n if (this.copyId) host.show(this.startLayers, sel, this.copyId);\n this.shown = host.layers();\n }\n\n /** The current geometry. */\n get geometry(): FreeTransformGeometry {\n return this.geometryValue;\n }\n\n get interpolation(): Interpolation {\n return this.interp;\n }\n\n get isOpen(): boolean {\n return !this.closed;\n }\n\n private check(): void {\n if (this.closed) fail('This transform has already been committed or cancelled; begin a new one.');\n if (this.host.layers() !== this.shown) {\n this.closed = true;\n fail('The document changed while the transform was open, so it was abandoned; begin a new one.');\n }\n }\n\n /**\n * Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,\n * shows the result live (no history). Returns the geometry.\n */\n update(changes: { corners?: FreeTransformGeometry['corners']; warp?: FreeTransformGeometry['warp']; interpolation?: Interpolation }, preview: FreeTransformPreview | false = {}): FreeTransformGeometry {\n this.check();\n if (changes.interpolation !== undefined) {\n if (!isInterpolation(changes.interpolation)) fail(`Unknown interpolation \"${String(changes.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = changes.interpolation;\n }\n const next: FreeTransformGeometry = {\n box: this.box,\n corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })) as unknown as FreeTransformGeometry['corners'],\n warp: changes.warp === undefined ? this.geometryValue.warp ?? null : changes.warp,\n };\n if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail('corners must be four finite points: top-left, top-right, bottom-right, bottom-left.');\n if (!transformMap(next)) fail('These corners fold the frame over itself or through the horizon; keep the quad convex.');\n this.geometryValue = next;\n if (preview !== false) this.render(preview);\n return next;\n }\n\n /** Resamples every part for the current geometry and shows it (or returns it, for commit). */\n private resample(final: boolean, preview: FreeTransformPreview = {}): { layers: ImageLayer[]; selection: SelectionMask | null } {\n if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };\n const g = this.geometryValue;\n const identity = isIdentityTransform(g);\n const warp = hasWarp(g);\n const h = warp ? null : geometryHomography(g);\n const f = warp ? transformMap(g) : null;\n const step = final ? 1 : previewStep(preview.scale);\n const interpolation: Interpolation = final ? this.interp : this.interp === 'nearest' ? 'nearest' : 'bilinear';\n let layers = this.startLayers;\n let selection = this.startSelection;\n const targetId = this.copyId ?? this.layerId;\n for (const part of this.parts) {\n const o = part.offset;\n let moved: TiledSurface;\n if (identity) moved = part.moving;\n else {\n let src = part.moving;\n let k = 1;\n if (step > 1) {\n k = step;\n let r = part.reduced.get(k);\n if (!r) {\n r = reduceSurface(part.moving, k);\n part.reduced.set(k, r);\n }\n src = r;\n }\n const opts: ResampleOptions = { interpolation, step: step > 1 ? step : undefined };\n if (preview.clip && !final) opts.clip = { x: Math.floor(preview.clip.x - o.x), y: Math.floor(preview.clip.y - o.y), width: Math.ceil(preview.clip.width) + 1, height: Math.ceil(preview.clip.height) + 1 };\n try {\n if (f) {\n moved = warpSurface(src, (p) => {\n const q = f({ x: p.x * k + o.x, y: p.y * k + o.y });\n return q && { x: q.x - o.x, y: q.y - o.y };\n }, opts);\n } else {\n // Layer space → document → transform → layer space, from the reduced copy's pixels.\n const hl = multiplyHomographies(multiplyHomographies(multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], T(o.x, o.y)), h!), T(-o.x, -o.y));\n moved = transformSurface(src, hl, opts);\n }\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;\n if (part.what === 'selection') {\n const { width, height } = this.host.canvas();\n const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);\n const next = createSelectionMask(width, height, data);\n selection = next.bounds ? next : null;\n } else if (part.what === 'pixels') {\n layers = mapLayer(layers, targetId!, (l) => {\n const next = { ...(l as PixelLayer), surface: result };\n delete next.source;\n return next;\n });\n } else {\n layers = mapLayer(layers, targetId!, (l) => ({ ...l, mask: { ...(l.mask as RasterMask), surface: result } }) as ImageLayer);\n }\n }\n return { layers, selection };\n }\n\n /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */\n private resampleSmart(final: boolean, preview: FreeTransformPreview): ImageLayer[] {\n const layer = this.smart!;\n const g = this.geometryValue;\n const placement = { corners: g.corners, warp: g.warp ?? null, interpolation: this.interp };\n const canvas = this.host.canvas();\n try {\n if (final) {\n const res = setSmartPlacement(this.startLayers, layer.id, placement, canvas);\n this.smartMasksMoved = res.masksMoved;\n return res.layers;\n }\n const step = previewStep(preview.scale);\n const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });\n return mapLayer(this.startLayers, layer.id, (l) => ({ ...(l as SmartObjectLayer), placement, surface }));\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n\n /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */\n private isChanged(): boolean {\n const g = this.geometryValue;\n if (!this.smart) return !isIdentityTransform(g);\n const a = this.startGeometry;\n const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);\n const warp = (w: FreeTransformGeometry['warp'], box: CropBox) => (w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : '');\n return !same || warp(g.warp, g.box) !== warp(a.warp, a.box) || this.interp !== this.startInterp;\n }\n\n private render(preview: FreeTransformPreview): void {\n const { layers, selection } = this.resample(false, preview);\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.shown = this.host.layers();\n }\n\n /** Finishes the transform with full-quality resampling, as one undo step. */\n commit(): FreeTransformResult {\n this.check();\n const changed = this.isChanged() || Boolean(this.copyId);\n if (!changed) {\n this.closed = true;\n this.host.restore(this.before);\n return this.result(false, this.startLayers, this.startSelection);\n }\n const { layers, selection } = this.resample(true);\n this.closed = true;\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.host.record(this.before);\n return this.result(true, layers, selection);\n }\n\n /** Abandons the transform and puts everything back (no history). */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.host.restore(this.before);\n }\n\n private result(changed: boolean, layers: ImageLayer[], selection: SelectionMask | null): FreeTransformResult {\n let bounds: CropBox | null = null;\n const id = this.copyId ?? this.layerId;\n const layer = id ? findLayer(layers, id)?.layer : undefined;\n if (this.moves.includes('pixels') && layer?.type === 'pixel') bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);\n else if (this.moves.includes('pixels') && layer?.type === 'smart') bounds = contentBounds(layer.surface);\n else if (this.moves.includes('mask') && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 });\n else if (this.moves.includes('selection')) bounds = selection?.bounds ? { ...selection.bounds } : null;\n return {\n layerId: this.copyId ?? this.layerId,\n content: this.content,\n moved: this.smart ? this.moves.filter((m) => m !== 'mask' || this.smartMasksMoved) : [...this.moves],\n box: { ...this.box },\n corners: this.geometryValue.corners,\n bounds,\n changed,\n };\n }\n}\n","import {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentParamsMap,\n Annotation,\n CreateAnnotationPayload,\n AspectRatio,\n CropBox,\n DepthMaskOptions,\n ExportOptions,\n ImageAdjustments,\n ImageDimensions,\n ImageEditorFit,\n ImageEditorOptions,\n ImageEditorState,\n ImageEditorTool,\n ImageTransform,\n ImagePoint,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { ImageEditorRenderer, RenderOptions } from './renderer';\nimport { clampCropToImage, isQuarterTurnSwapped } from './geometry';\nimport { LATEST_PROCESS_VERSION, LEGACY_PROCESS_VERSION, normalizeProcessVersion } from './color';\nimport { createAdjustmentLayer, normalizeAdjustmentLayer, normalizeAdjustmentLayers } from './adjustment-layers';\nimport type { BlendMode } from './blend';\nimport type { ImageHistogram } from './histogram';\nimport { applyAdjustments } from './filters';\nimport {\n combineSelections,\n createSelectionMask,\n deserializeSelection,\n fullSelection,\n invertSelection as invertSelectionMask,\n resizeSelection,\n selectionFromPixels,\n serializeSelection,\n type PixelBuffer,\n type SelectionChannel,\n type SelectionMask,\n type SelectionMode,\n} from './selection/mask';\nimport { ellipseSelection, polygonSelection, rectSelection, type ShapeSelectionOptions } from './selection/shapes';\nimport { colorRange, magicWand, type ColorRangeOptions, type ColorRangeSample, type MagicWandOptions } from './selection/wand';\nimport { modifySelection as modifySelectionMask, type SelectionModifyOp } from './selection/modify';\nimport { refineSelection as refineSelectionMask, type RefineEdgeParams } from './selection/refine';\nimport type { LayerMaskProps } from './selection/layer-mask';\nimport type { AdjustmentLayerMask } from './types';\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES, DEFAULT_TRANSFORM_VALUES } from './layers/defaults';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type GroupLayer,\n type LayerBlendMode,\n type ImageLayer,\n type LayerFilter,\n type LayerTarget,\n type PixelLayer,\n type RasterMask,\n type SmartContents,\n type SmartFilter,\n type SmartObjectLayer,\n type SmartPlacement,\n} from './layers/model';\nimport {\n addSmartFilter as addSmartFilterOp,\n contentsSurface,\n convertToSmartObject as convertToSmartObjectOp,\n copySmartObject as copySmartObjectOp,\n documentContents,\n instancesOf,\n isContentsDecoded,\n moveSmartFilter as moveSmartFilterOp,\n normalizeSmartFilter,\n offsetSmartObject,\n placementGeometry,\n rasterContents,\n rasterizeSmartObject as rasterizeSmartObjectOp,\n removeSmartFilter as removeSmartFilterOp,\n replaceContents as replaceContentsOp,\n requireSmartObject,\n setSmartFilterMask as setSmartFilterMaskOp,\n setSmartFiltersEnabled as setSmartFiltersEnabledOp,\n setSmartPlacement,\n updateSmartFilter as updateSmartFilterOp,\n withFilterMaskSurface,\n} from './layers/smart';\nimport {\n BACKGROUND_LAYER_ID,\n BACKGROUND_LAYER_NAME,\n BLUR_FILTER_ID,\n DEVELOP_FILTER_ID,\n adjustmentNodeFromLegacy,\n annotationLayerFor,\n annotationLayerName,\n depthRasterMask,\n developTarget,\n isDefaultDepthMask,\n layersFromLegacy,\n legacyMaskOf,\n legacyView,\n rasterMaskFromLegacy,\n type LayerStack,\n} from './layers/migrate';\nimport {\n LAYERED_SCHEMA_VERSION,\n deserializeLayers,\n deserializePatterns,\n 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 tileKey,\n} from '../raster/surface';\nimport {\n LayerOperationError,\n addLayerEffect as addLayerEffectOp,\n addLayerFilter as addLayerFilterOp,\n layerEffectsOf,\n removeLayerEffect as removeLayerEffectOp,\n setLayerEffects as setLayerEffectsOp,\n setLayerEffectsEnabled as setLayerEffectsEnabledOp,\n updateLayerEffect as updateLayerEffectOp,\n duplicateLayer as duplicateLayerOp,\n groupLayers as groupLayersOp,\n moveLayer as moveLayerOp,\n normalizeFilter,\n offsetLayer as offsetLayerOp,\n parseBlend,\n placeLayer,\n removeLayer as removeLayerOp,\n removeLayerFilter as removeLayerFilterOp,\n requireLayer,\n requireUnlocked,\n setLayerMask as setLayerMaskOp,\n setLayerProperties as setLayerPropertiesOp,\n ungroupLayer as ungroupLayerOp,\n unionRect,\n updateLayerFilter as updateLayerFilterOp,\n type LayerPlacement,\n type LayerProperties,\n} from './layers/ops';\nimport { coveringRect } from './layers/render';\nimport {\n CoverageMap,\n DEFAULT_BRUSH,\n DEFAULT_MIXER,\n EMPTY_MIXER_LOAD,\n MixerStroke,\n StrokeCoverage,\n applyStrokeToTile,\n cleanMixer,\n loadMixer,\n normalizeBrush,\n normalizeMixer,\n type BrushSettings,\n type MixerLoad,\n type MixerSettings,\n type MixerTarget,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, encodePng, isPng, parseDataUrl, 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, 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}\n\n/** Paint tools a stroke can use. */\nexport type StrokeTool = 'brush' | 'eraser' | 'mixer';\n\n/** A stroke: tool, target (default the active layer and target) and brush / colour overrides. */\nexport interface StrokeInit {\n tool?: StrokeTool;\n layerId?: string;\n channel?: LayerTarget;\n /** Overrides of the paint options' brush. */\n brush?: Partial<BrushSettings>;\n /**\n * Paint colour (default the foreground). Masks take its grey value. For the mixer, the colour\n * the reservoir is loaded with: given, it loads the brush whatever `loadEachStroke` says.\n */\n color?: string | number[];\n /** Overrides of the paint options' Mixer Brush settings (tool `mixer`). */\n mixer?: Partial<MixerSettings>;\n /** Eraser colour on locked transparency / masks (default the background). */\n background?: string | number[];\n /** Kept with the stroke for replays (v1 brushes have no jitter). */\n seed?: number;\n}\n\nexport interface FillOptions {\n layerId?: string;\n channel?: LayerTarget;\n /** Default the foreground colour. */\n color?: string | number[];\n /** 0..1 (default 1). */\n opacity?: number;\n blend?: BlendMode;\n /** Limit to the selection when there is one (default true). */\n useSelection?: boolean;\n}\n\n/** What a stroke, fill or clear changed. */\nexport interface PaintStrokeResult {\n layerId: string;\n channel: LayerTarget;\n /** Document rectangle that changed (null when nothing did). */\n bounds: CropBox | null;\n dabs: number;\n /** The Mixer Brush's load after the stroke (tool `mixer` only). */\n mixerLoad?: MixerLoad;\n}\n\n/** An opaque pre-stroke history snapshot. */\nexport type HistoryEntryHandle = { readonly __historyEntry: unique symbol };\n\n/** Composites `coverage` into the tiles of `base` under `rects` (document space), returning the new surface. */\nfunction paintCoverageInto(base: TiledSurface, coverage: CoverageMap, apply: StrokeApply, rects: CropBox[]): TiledSurface {\n const tiles = new Map(base.tiles);\n const done = new Set<TileKey>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n if (done.has(key)) continue;\n done.add(key);\n const src = base.tiles.get(key)?.data ?? defaultTile(base);\n const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);\n const tile = finishTile(data, base.channels, base.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n }\n return new TiledSurfaceClass(base.channels, tiles, base.defaultValue);\n}\n\nfunction defaultTile(s: TiledSurface): Uint8ClampedArray {\n const d = new Uint8ClampedArray(TILE * TILE * s.channels);\n if (s.defaultValue) d.fill(s.defaultValue);\n return d;\n}\n\n/** How a Mixer Brush stroke starts (see `PaintStrokeSession`). */\nexport interface MixerStrokeInit {\n /** Makes the stroke over the session's live pixels. */\n create(pixels: MixerTarget): MixerStroke;\n /** Stores the brush load when the stroke ends and returns it (after any clean). */\n end(load: MixerLoad): MixerLoad;\n}\n\n/**\n * A stroke in progress (see `beginStroke`): `push` input points as they arrive, then `end` (one\n * undo step) or `cancel`. Brush and eraser strokes build up a coverage map composited over the\n * pre-stroke pixels; Mixer Brush strokes paint dab by dab into the live pixels, so each dab picks\n * up what the dabs before it laid down.\n */\nexport class PaintStrokeSession {\n private readonly before: HistoryEntryHandle;\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private readonly stroke: StrokeCoverage | null;\n private readonly mixer: MixerStroke | null;\n private readonly mixerInit: MixerStrokeInit | null = null;\n /** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */\n private readonly fresh = new Set<TileKey>();\n private last: StrokePoint | null = null;\n private closed = false;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly target: { id: string; channel: LayerTarget; surface: TiledSurface },\n stroke: StrokeCoverage | MixerStrokeInit,\n private readonly apply: StrokeApply\n ) {\n this.before = controller._captureForStroke();\n this.base = target.surface;\n this.live = new Map(this.base.tiles);\n if (stroke instanceof StrokeCoverage) {\n this.stroke = stroke;\n this.mixer = null;\n } else {\n this.stroke = null;\n this.mixer = stroke.create(this.mixerPixels());\n this.mixerInit = stroke;\n }\n }\n\n /** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */\n private mixerPixels(): MixerTarget {\n const a = this.apply;\n return {\n offset: a.offset,\n lockTransparency: a.lockTransparency,\n selection: a.selection,\n readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? (this.base.defaultValue ? defaultTile(this.base) : null),\n writeTile: (tx, ty) => {\n const key = tileKey(tx, ty);\n if (!this.touched.has(key)) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n this.live.set(key, { data: new Uint8ClampedArray(src), uniform: null });\n this.touched.add(key);\n }\n this.fresh.add(key);\n return this.live.get(key)!.data;\n },\n };\n }\n\n get layerId(): string {\n return this.target.id;\n }\n\n /** Adds input points (document pixels); the stack shows the stroke so far. */\n push(points: readonly StrokePoint[]): void {\n if (this.closed || !points.length) return;\n this.last = points[points.length - 1];\n this.update(this.mixer ? this.mixer.push(points) : this.stroke!.push(points));\n }\n\n private update(rects: CropBox[]): void {\n if (!rects.length) return;\n const ch = this.base.channels;\n if (this.mixer) {\n for (const key of this.fresh) this.live.set(key, { data: this.live.get(key)!.data, uniform: null });\n this.fresh.clear();\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n return;\n }\n const stroke = this.stroke!;\n // Per target tile, the union of the new dabs inside it: only that part is recomputed.\n const regions = new Map<TileKey, { tx: number; ty: number; r: CropBox }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n const cur = regions.get(key);\n regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r)! : r });\n }\n }\n for (const [key, { tx, ty, r }] of regions) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n const prev = this.touched.has(key) ? this.live.get(key)?.data : undefined;\n this.touched.add(key);\n const data = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Finishes the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new LayerOperationError('This stroke has already ended.');\n const last = this.last ?? undefined;\n this.update(this.mixer ? this.mixer.finish(last) : this.stroke!.finish(last));\n this.closed = true;\n const ch = this.base.channels;\n const tiles = new Map(this.base.tiles);\n let changed = false;\n for (const key of this.touched) {\n const live = this.live.get(key)!;\n const tile = finishTile(live.data, ch, this.base.defaultValue);\n const prev = this.base.tiles.get(key);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n if (!sameTile(prev, tile)) changed = true;\n }\n const surface = changed ? new TiledSurfaceClass(ch, tiles, this.base.defaultValue) : null;\n const mixerLoad = this.mixer ? this.mixerInit!.end(this.mixer.load) : undefined;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);\n const b = this.mixer ? this.mixer.bounds : this.stroke!.coverage.bounds;\n const result: PaintStrokeResult = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke!).dabCount };\n if (mixerLoad) result.mixerLoad = mixerLoad;\n return result;\n }\n\n /** Abandons the stroke and restores the layer. */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);\n }\n}\n\nfunction sameTile(a: Tile | undefined, b: Tile | null): boolean {\n if (!a || !b) return !a && !b;\n if (a === b) return true;\n if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;\n for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;\n return true;\n}\n\n/** Where a new annotation goes. */\nexport interface AnnotationPlacement {\n /** Put the annotation layer directly above this layer (default: the top of the stack). */\n aboveId?: string;\n}\n\n/** The image a history entry was recorded against. */\ninterface SourceSnapshot {\n sourceUrl: string | null;\n element: CanvasImageSource | null;\n dimensions: ImageDimensions;\n}\n\n/**\n * Undo/redo entry: the document as immutable objects. Layers, surfaces, tiles and selections are\n * shared with the other entries and the current state, so an entry costs about the tiles it changed.\n */\ninterface HistoryEntry {\n layers: ImageLayer[];\n activeLayerId: string | null;\n activeTarget: LayerTarget;\n crop: CropBox | null;\n transform: ImageTransform;\n outputOpacity: number;\n legacyOutputBlur?: number;\n globalLight?: GlobalLight;\n patterns?: EffectPattern[];\n selection: SelectionMask | null;\n source: SourceSnapshot;\n}\n\nexport const DEFAULT_ADJUSTMENTS: ImageAdjustments = DEFAULT_ADJUSTMENTS_VALUES;\n\n/**\n * Fills in defaults for stored adjustments. A stored object without `process` was saved before\n * process versions existed and keeps the legacy rendering.\n */\nexport function normalizeStoredAdjustments(stored: Partial<ImageAdjustments> | undefined | null): ImageAdjustments {\n if (!stored) return { ...DEFAULT_ADJUSTMENTS };\n const out: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS,\n ...stored,\n process: normalizeProcessVersion(stored.process),\n };\n // Layers are deep-copied and cleaned; an empty or missing list is left out entirely.\n const layers = normalizeAdjustmentLayers(stored.layers);\n if (layers.length) out.layers = layers;\n else delete out.layers;\n return out;\n}\n\nconst isLayered = (data: { version?: unknown } | null | undefined): data is SerializedLayeredImageEditorState =>\n Boolean(data && data.version === LAYERED_SCHEMA_VERSION && Array.isArray((data as { layers?: unknown }).layers));\n\n/**\n * Brings a stored editor state of any schema version to a clean state of the same family:\n * version 1 (and a missing version) has no adjustment layers, version 2 has them, and both stay\n * in the document-level schema so old states load and render exactly as before. Version 3\n * (layers) is returned with its document fields cleaned. To lift a v1 / v2 state into layers,\n * use {@link migrateToLayeredState}.\n */\nexport function migrateSerializedImageEditorState(\n data: Partial<SerializedImageEditorState> & { version?: number }\n): SerializedImageEditorState {\n if (isLayered(data)) {\n const out: SerializedLayeredImageEditorState = {\n ...data,\n crop: data.crop ? { ...data.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...data.transform },\n opacity: typeof data.opacity === 'number' ? Math.min(1, Math.max(0, data.opacity)) : 1,\n assets: { ...(data.assets ?? {}) },\n activeLayerId: typeof data.activeLayerId === 'string' ? data.activeLayerId : null,\n };\n return out;\n }\n const legacy = data as Partial<LegacySerializedImageEditorState>;\n const adjustments = normalizeStoredAdjustments(legacy.adjustments);\n return {\n version: adjustments.layers?.length ? 2 : 1,\n crop: legacy.crop ? { ...legacy.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...legacy.transform },\n adjustments: serializeAdjustments(adjustments),\n depthMask: {\n ...DEFAULT_DEPTH_MASK,\n ...legacy.depthMask,\n depthRange: [...(legacy.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange)] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(legacy.annotations ?? [])),\n ...(legacy.selection ? { selection: { ...legacy.selection } } : {}),\n };\n}\n\n/**\n * Lifts a stored state of any version into the layered schema (v3) for a canvas of\n * `width × height`: the source becomes the Background layer with the base sliders as its\n * `develop` filter and the depth mask as its parametric mask, adjustment layers go above it and\n * annotations on top (see `layersFromLegacy`).\n */\nexport function migrateToLayeredState(\n data: Partial<SerializedImageEditorState> & { version?: number },\n size: ImageDimensions\n): SerializedLayeredImageEditorState {\n const clean = migrateSerializedImageEditorState(data);\n if (isLayered(clean)) return clean;\n const stack = layersFromLegacy(clean, Math.round(size.width), Math.round(size.height));\n const { layers, assets } = serializeLayers(stack.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width: Math.round(size.width),\n height: Math.round(size.height),\n layers,\n assets,\n activeLayerId: null,\n crop: clean.crop,\n transform: clean.transform,\n opacity: stack.outputOpacity,\n };\n if (stack.legacyOutputBlur !== undefined) out.legacyOutputBlur = stack.legacyOutputBlur;\n if (clean.selection) out.selection = clean.selection;\n return out;\n}\n\nexport const DEFAULT_TRANSFORM: ImageTransform = DEFAULT_TRANSFORM_VALUES;\n\nexport const DEFAULT_DEPTH_MASK: DepthMaskOptions = DEFAULT_DEPTH_MASK_VALUES;\n\nconst DEVELOP_KEYS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/**\n * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,\n * tool interaction states, undo/redo history, serialization, and image export.\n */\nexport class ImageEditorController {\n private state: ImageEditorState;\n private renderer: ImageEditorRenderer;\n private imageElement: CanvasImageSource | null = null;\n private listeners = new Set<(state: ImageEditorState) => void>();\n private histogramListeners = new Set<(histogram: ImageHistogram | null) => void>();\n private onStateChange?: (state: ImageEditorState) => void;\n\n // History stacks\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private maxHistoryDepth: number;\n private maxHistoryBytes: number;\n private isApplyingHistory = false;\n private initialSnapshot: HistoryEntry;\n /** Pixels the selection tools sample (host-provided or read back from the source). */\n private samplePixels: PixelBuffer | null = null;\n private samplePixelsOverride = false;\n /** Decoded source pixels (the Background's surface), by the buffer or element they came from. */\n private sourceSurfaces = new WeakMap<object, PixelSurface>();\n private viewKey: unknown[] = [];\n /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */\n private compositor = new LayerCompositor();\n\n constructor(options: ImageEditorOptions = {}) {\n this.maxHistoryDepth = options.maxHistoryDepth ?? 50;\n this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;\n this.onStateChange = options.onStateChange;\n this.renderer = new ImageEditorRenderer();\n this.renderer.onPreviewHistogram = (histogram) => {\n this.histogramListeners.forEach((l) => l(histogram));\n };\n this.renderer.layerSources = {\n surfaceOf: (layer) => this.surfaceOf(layer),\n sourceIsOpaque: () => this.sourceIsOpaque(),\n version: () => `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: () => this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n\n this.state = {\n sourceUrl: typeof options.image === 'string' ? options.image : null,\n imageDimensions: { width: 800, height: 600 },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM },\n adjustments: { ...DEFAULT_ADJUSTMENTS },\n depthMask: { ...DEFAULT_DEPTH_MASK },\n annotations: [],\n selectedAnnotationId: null,\n activeTool: 'select',\n zoom: 1,\n pan: { x: 0, y: 0 },\n isOriginalCompared: false,\n selection: null,\n quickMask: false,\n layers: [],\n activeLayerId: null,\n activeTarget: 'pixels',\n outputOpacity: 1,\n paint: { brush: { ...DEFAULT_BRUSH }, foreground: '#000000', background: '#ffffff', mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },\n fit: options.fit ?? 'margin',\n };\n\n if (options.image instanceof Object && 'width' in options.image) {\n this.imageElement = options.image as CanvasImageSource;\n this.state.sourceUrl = (options.image as any).src || null;\n this.state.imageDimensions = {\n width: (options.image as any).naturalWidth || (options.image as any).width || 800,\n height: (options.image as any).naturalHeight || (options.image as any).height || 600,\n };\n }\n\n this.applyStored(options.initialState ?? {});\n if (options.initialState?.selection) {\n try {\n this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));\n } catch {\n // A damaged selection is dropped; the rest of the state still loads.\n }\n }\n\n this.initialSnapshot = this.captureHistoryEntry();\n\n if (typeof options.image === 'string') {\n this.loadImage(options.image);\n }\n }\n\n // --- Image Loading ---\n\n /**\n * Loads a new source image and makes it the baseline that `reset()` returns to.\n * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`\n * for undoable source replacement.\n */\n public async loadImage(src: string | HTMLImageElement): Promise<void> {\n // Keep the URL visible synchronously (UI bindings compare against it while loading).\n this.abandonContents();\n if (typeof src === 'string') this.state.sourceUrl = src;\n const source = await this.resolveSource(src);\n const prev = this.state.imageDimensions;\n this.imageElement = source.element;\n this.state.sourceUrl = source.sourceUrl;\n this.state.imageDimensions = source.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (prev.width !== source.dimensions.width || prev.height !== source.dimensions.height) {\n // Masks lifted from v1 / v2 documents follow the image, as they did when they were resampled at render.\n this.state.layers = resizeLegacyMasks(this.state.layers, prev, source.dimensions);\n }\n this.dropMismatchedSelection();\n\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n }\n\n private async resolveSource(src: string | HTMLImageElement): Promise<SourceSnapshot> {\n if (typeof src === 'string') {\n try {\n const img = await this.renderer.imageCache.get(src);\n return {\n sourceUrl: src,\n element: img,\n dimensions: {\n width: img.naturalWidth || img.width || 800,\n height: img.naturalHeight || img.height || 600,\n },\n };\n } catch {\n // In SSR / non-DOM environments, retain current dimensions\n return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };\n }\n }\n return {\n sourceUrl: (src as any).src || null,\n element: src,\n dimensions: {\n width: src.naturalWidth || src.width || 800,\n height: src.naturalHeight || src.height || 600,\n },\n };\n }\n\n public getImageElement(): CanvasImageSource | null {\n return this.imageElement;\n }\n\n // --- State & Subscription ---\n\n public getState(): ImageEditorState {\n this.syncViews();\n return this.state;\n }\n\n public subscribe(listener: (state: ImageEditorState) => void): () => void {\n this.syncViews();\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Where layer pixels changed since the last viewport render, when only strokes changed them\n * (the renderer then recomposites just those rectangles); `'all'` after any other change.\n */\n private pendingDirty: CropBox[] | 'all' = 'all';\n\n private notify(strokeRects?: CropBox[]): void {\n if (strokeRects && this.pendingDirty !== 'all') this.pendingDirty.push(...strokeRects);\n else if (!strokeRects) this.pendingDirty = 'all';\n this.syncViews();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n /** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */\n private syncViews(): void {\n const s = this.state;\n const key = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];\n if (key.length === this.viewKey.length && key.every((v, i) => v === this.viewKey[i])) return;\n this.viewKey = key;\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, s.activeLayerId);\n s.adjustments = view.adjustments;\n s.depthMask = view.depthMask;\n s.annotations = view.annotations;\n }\n\n private stack(): LayerStack {\n const out: LayerStack = { layers: this.state.layers, outputOpacity: this.state.outputOpacity };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n\n // --- History (Undo / Redo / Reset) ---\n\n private captureHistoryEntry(): HistoryEntry {\n const s = this.state;\n const entry: HistoryEntry = {\n layers: s.layers,\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n crop: s.crop ? { ...s.crop } : null,\n transform: { ...s.transform },\n outputOpacity: s.outputOpacity,\n selection: s.selection ?? null,\n source: {\n sourceUrl: s.sourceUrl,\n element: this.imageElement,\n dimensions: { ...s.imageDimensions },\n },\n };\n if (s.legacyOutputBlur !== undefined) entry.legacyOutputBlur = s.legacyOutputBlur;\n if (s.globalLight) entry.globalLight = { ...s.globalLight };\n if (s.patterns) entry.patterns = s.patterns;\n return entry;\n }\n\n private restoreHistoryEntry(entry: HistoryEntry): void {\n const s = this.state;\n s.layers = entry.layers;\n s.activeLayerId = entry.activeLayerId;\n s.activeTarget = entry.activeTarget;\n s.crop = entry.crop ? { ...entry.crop } : null;\n s.transform = { ...entry.transform };\n s.outputOpacity = entry.outputOpacity;\n if (entry.legacyOutputBlur !== undefined) s.legacyOutputBlur = entry.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n if (entry.globalLight) s.globalLight = { ...entry.globalLight };\n else delete s.globalLight;\n if (entry.patterns) s.patterns = entry.patterns;\n else delete s.patterns;\n s.selection = entry.selection;\n if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;\n this.imageElement = entry.source.element;\n s.sourceUrl = entry.source.sourceUrl;\n s.imageDimensions = { ...entry.source.dimensions };\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) {\n s.selectedAnnotationId = null;\n }\n }\n\n private recordHistory(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(this.captureHistoryEntry());\n if (this.undoStack.length > this.maxHistoryDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n\n /**\n * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest\n * undo steps are dropped while it exceeds `maxHistoryBytes`.\n */\n public get historyBytes(): number {\n const live = new Set<Tile>();\n collectStateTiles(this.state.layers, live);\n const held = new Set<Tile>();\n for (const e of [...this.undoStack, ...this.redoStack]) collectStateTiles(e.layers, held);\n let n = 0;\n for (const t of held) if (!live.has(t)) n += t.data.byteLength;\n return n;\n }\n\n private trimHistoryBytes(): void {\n if (!(this.maxHistoryBytes > 0) || this.undoStack.length <= 1) return;\n while (this.undoStack.length > 1 && this.historyBytes > this.maxHistoryBytes) this.undoStack.shift();\n }\n\n public get canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n public get canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n public undo(): boolean {\n if (!this.canUndo) return false;\n this.isApplyingHistory = true;\n this.redoStack.push(this.captureHistoryEntry());\n\n const prev = this.undoStack.pop()!;\n this.restoreHistoryEntry(prev);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public redo(): boolean {\n if (!this.canRedo) return false;\n this.isApplyingHistory = true;\n this.undoStack.push(this.captureHistoryEntry());\n\n const next = this.redoStack.pop()!;\n this.restoreHistoryEntry(next);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public reset(): void {\n this.recordHistory();\n this.restoreHistoryEntry(this.initialSnapshot);\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.state.activeTool = 'select';\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n // --- Active Tool ---\n\n public setTool(tool: ImageEditorTool): void {\n if (this.state.activeTool === tool) return;\n this.state.activeTool = tool;\n if (tool !== 'select') {\n this.state.selectedAnnotationId = null;\n }\n // If switching to crop tool and no crop is defined, initialize crop to full image\n if (tool === 'crop' && !this.state.crop) {\n this.state.crop = {\n x: 0,\n y: 0,\n width: this.state.imageDimensions.width,\n height: this.state.imageDimensions.height,\n };\n }\n this.notify();\n }\n\n // --- Crop ---\n\n public setCrop(crop: CropBox | null, addToHistory = true): void {\n if (addToHistory) {\n this.recordHistory();\n }\n this.state.crop = crop;\n this.notify();\n }\n\n public applyCropPreset(ratio: AspectRatio): void {\n this.recordHistory();\n const { width, height } = this.state.imageDimensions;\n // `ratio` is the on-screen aspect; the crop box lives in natural image space.\n if (ratio && isQuarterTurnSwapped(this.state.transform)) {\n ratio = 1 / ratio;\n }\n if (!ratio) {\n // Freeform: full image\n this.state.crop = { x: 0, y: 0, width, height };\n } else {\n let targetW = width;\n let targetH = targetW / ratio;\n\n if (targetH > height) {\n targetH = height;\n targetW = targetH * ratio;\n }\n\n const x = Math.max(0, Math.round((width - targetW) / 2));\n const y = Math.max(0, Math.round((height - targetH) / 2));\n this.state.crop = {\n x,\n y,\n width: Math.round(targetW),\n height: Math.round(targetH),\n };\n }\n this.notify();\n }\n\n public resetCrop(): void {\n this.recordHistory();\n this.state.crop = null;\n this.notify();\n }\n\n // --- Transformations (Rotate / Flip) ---\n\n public rotate(degrees: number): void {\n this.recordHistory();\n const current = this.state.transform.rotate;\n this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };\n this.notify();\n }\n\n public setRotation(degrees: number): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, rotate: degrees % 360 };\n this.notify();\n }\n\n public flipHorizontal(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };\n this.notify();\n }\n\n public flipVertical(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };\n this.notify();\n }\n\n // --- Layers (read) ---\n\n /** The layer stack, bottom to top (immutable objects). */\n public getLayers(): ImageLayer[] {\n return this.state.layers;\n }\n\n public getLayer(id: string): ImageLayer | undefined {\n return findLayer(this.state.layers, id)?.layer;\n }\n\n /** The active layer, or `undefined`. */\n public getActiveLayer(): ImageLayer | undefined {\n return this.state.activeLayerId ? this.getLayer(this.state.activeLayerId) : undefined;\n }\n\n /**\n * Makes a layer the active one (or none) and chooses whether painting targets its pixels or\n * its mask. View state: no undo step. Returns false for an unknown id.\n */\n public setActiveLayer(id: string | null, target: LayerTarget = 'pixels'): boolean {\n if (id !== null && !this.getLayer(id)) return false;\n const layer = id ? this.getLayer(id) : undefined;\n const t: LayerTarget = target === 'mask' && layer?.mask ? 'mask' : target === 'filter-mask' && layer?.type === 'smart' && layer.smartFilters?.mask ? 'filter-mask' : 'pixels';\n if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;\n this.state.activeLayerId = id;\n this.state.activeTarget = t;\n this.notify();\n return true;\n }\n\n /**\n * The pixels of a pixel layer, in document space (`x`, `y` is the top-left of the returned\n * buffer), or `null` for an unknown or empty layer. Resolves the Background's source pixels.\n */\n public getLayerPixels(id: string): (PixelBuffer & { x: number; y: number }) | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n const b = contentBounds(surface);\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height, data: readRegion(surface, b) };\n }\n\n private boundsCache = new WeakMap<object, CropBox | null>();\n\n /**\n * Document bounds of a layer's non-transparent pixels (pixel layers; the Background's source\n * included), or `null` for an empty or non-pixel layer.\n */\n public getLayerBounds(id: string): CropBox | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n let b = this.boundsCache.get(surface);\n if (b === undefined) {\n b = contentBounds(surface);\n this.boundsCache.set(surface, b);\n }\n return b ? { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height } : null;\n }\n\n /**\n * 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 || (layer.type !== 'pixel' && layer.type !== 'smart')) 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 or smart object composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer | SmartObjectLayer): PixelSurface {\n if (layer.type === 'smart' || layer.source !== 'document') return layer.surface;\n return this.sourceSurface() ?? layer.surface;\n }\n\n /**\n * The source image as a surface (cached): host-supplied sample pixels first, then the decoded\n * element, then pixels read back for the selection tools.\n */\n private sourceSurface(): PixelSurface | null {\n const { width, height } = this.naturalSize;\n const fromPixels = (px: PixelBuffer) => {\n let s = this.sourceSurfaces.get(px as object);\n if (!s) {\n s = surfaceFromPixels(px, 4);\n this.sourceSurfaces.set(px as object, s);\n }\n return s;\n };\n const px = this.samplePixels;\n const fits = Boolean(px && px.width === width && px.height === height);\n if (this.samplePixelsOverride && fits) return fromPixels(px!);\n const el = this.imageElement as unknown as object | null;\n if (el) {\n const cached = this.sourceSurfaces.get(el);\n if (cached) return cached;\n const decoded = this.decodeSource();\n if (decoded) {\n if (!this.samplePixelsOverride) this.samplePixels = decoded;\n const s = surfaceFromPixels(decoded, 4);\n this.sourceSurfaces.set(el, s);\n this.sourceSurfaces.set(decoded as object, s);\n return s;\n }\n }\n return fits ? fromPixels(px!) : null;\n }\n\n private opaqueCache = new WeakMap<object, boolean>();\n\n /** Whether every source pixel is opaque (some legacy renders depend on it). */\n private sourceIsOpaque(): boolean {\n const s = this.sourceSurface();\n if (!s) return true;\n let v = this.opaqueCache.get(s);\n if (v === undefined) {\n const { width, height } = this.naturalSize;\n v = isOpaqueInside(s, width, height);\n this.opaqueCache.set(s, v);\n }\n return v;\n }\n\n /** Synchronously reads the source element's pixels through a canvas, or `null`. */\n private decodeSource(): PixelBuffer | null {\n const el = this.imageElement;\n if (!el) return null;\n const { width, height } = this.naturalSize;\n try {\n const canvas = this.renderer.createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n ctx.drawImage(el, 0, 0, width, height);\n const data = ctx.getImageData(0, 0, width, height);\n if (!data?.data || data.data.length < width * height * 4) return null;\n return { width, height, data: data.data };\n } catch {\n return null;\n }\n }\n\n // --- Layer operations ---\n\n /** Records one undo step and applies a new stack (and any other state change in `extra`). */\n private commitLayers(next: ImageLayer[], extra?: () => void, commitToHistory = true): void {\n if (commitToHistory) this.recordHistory();\n this.state.layers = next;\n extra?.();\n if (this.state.activeLayerId && !findLayer(next, this.state.activeLayerId)) {\n this.state.activeLayerId = null;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n }\n\n /** The default position of a new layer: above the active layer, else the top of the stack. */\n private newLayerPlacement(placement: LayerPlacement): LayerPlacement {\n if (placement.aboveId || placement.belowId || placement.parentId || placement.index !== undefined) return placement;\n const active = this.state.activeLayerId ? findLayer(this.state.layers, this.state.activeLayerId) : null;\n return active ? { aboveId: active.layer.id } : {};\n }\n\n /**\n * Adds a pixel layer (one undo step) and makes it active: empty, filled with `color` over the\n * canvas, or holding `pixels` placed at `x`, `y`. Goes above the active layer unless placed.\n * Returns its id.\n */\n public addPixelLayer(init: PixelLayerInit = {}): string {\n const { width, height } = this.naturalSize;\n let surface = emptySurface(4);\n if (init.pixels) {\n const px = init.pixels;\n if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {\n throw new LayerOperationError('pixels must be RGBA with width × height × 4 bytes.');\n }\n surface = surfaceFromPixels(px, 4);\n } else if (init.color !== undefined) {\n const c = parseColor(init.color);\n surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));\n }\n const blend = parseBlend(init.blend) as BlendMode | undefined;\n if (init.opacity !== undefined && !(init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer: PixelLayer = {\n type: 'pixel',\n id: init.id && !ids.has(init.id) ? init.id : newLayerId(),\n name: init.name?.trim() || this.nextLayerName(),\n visible: init.visible ?? true,\n opacity: init.opacity ?? 1,\n blend: blend ?? 'normal',\n surface,\n offset: { x: Math.round(init.x ?? 0), y: Math.round(init.y ?? 0) },\n };\n if (init.fillOpacity !== undefined && init.fillOpacity !== 1) layer.fillOpacity = init.fillOpacity;\n const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = layer.id;\n this.state.activeTarget = 'pixels';\n });\n return layer.id;\n }\n\n /**\n * `addPixelLayer` from an image: a PNG data URL or base64 PNG is decoded without a canvas;\n * other formats and URLs need a browser. Returns the new layer id.\n */\n public async addPixelLayerFromImage(image: string, init: Omit<PixelLayerInit, 'pixels' | 'color'> = {}): Promise<string> {\n const pixels = await this.decodeImage(image);\n return this.addPixelLayer({ ...init, pixels });\n }\n\n /** RGBA pixels of an image given as a data URL, base64 PNG or URL. */\n public async decodeImage(image: string): Promise<PixelBuffer> {\n const trimmed = image.trim();\n let png: Uint8Array | null = null;\n if (/^data:image\\/png/i.test(trimmed)) png = parseDataUrl(trimmed)?.bytes ?? null;\n else if (/^[A-Za-z0-9+/=\\s]+$/.test(trimmed) && trimmed.length > 16) {\n try {\n const bytes = base64ToBytes(trimmed);\n if (isPng(bytes)) png = bytes;\n } catch {\n png = null;\n }\n }\n if (png) {\n try {\n const d = decodePng(png);\n return { width: d.width, height: d.height, data: d.data };\n } catch {\n // Fall back to the browser decoder below (interlaced PNGs).\n }\n }\n const src = png ? `data:image/png;base64,${bytesToBase64(png)}` : trimmed;\n let img: HTMLImageElement;\n try {\n img = await this.renderer.imageCache.get(src);\n } catch {\n throw new LayerOperationError('The image could not be decoded here. Pass a PNG (data URL or base64), or run in a browser for other formats and URLs.');\n }\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const ctx = this.renderer.createCanvas(w, h).getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') throw new LayerOperationError('Decoding this image needs a canvas; pass a PNG instead.');\n ctx.drawImage(img, 0, 0, w, h);\n const data = ctx.getImageData(0, 0, w, h);\n return { width: w, height: h, data: data.data };\n }\n\n private nextLayerName(): string {\n const names = new Set(flattenLayers(this.state.layers).map((l) => l.name));\n let n = 1;\n while (names.has(`Layer ${n}`)) n++;\n return `Layer ${n}`;\n }\n\n /** Duplicates a layer (with its children) directly above it, as one undo step. Returns the copy's id. */\n public duplicateLayer(id: string, name?: string): string {\n const res = duplicateLayerOp(this.state.layers, id, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Deletes a layer (refused when locked), as one undo step. */\n public removeLayer(id: string): void {\n const next = removeLayerOp(this.state.layers, id);\n this.commitLayers(next, () => {\n if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === 'annotation' && l.annotation.id === this.state.selectedAnnotationId)) {\n this.state.selectedAnnotationId = null;\n }\n });\n }\n\n /** Moves a layer above / below another, or into a group, as one undo step. Returns its new parent and index. */\n public moveLayer(id: string, placement: LayerPlacement): { parentId: string | null; index: number } {\n const next = moveLayerOp(this.state.layers, id, placement);\n const loc = findLayer(next, id)!;\n if (next === this.state.layers) return { parentId: loc.parent?.id ?? null, index: loc.index };\n this.commitLayers(next);\n return { parentId: loc.parent?.id ?? null, index: loc.index };\n }\n\n /**\n * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as\n * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.\n */\n public setLayerProperties(id: string, props: LayerProperties, commitToHistory = true): ImageLayer {\n const next = setLayerPropertiesOp(this.state.layers, id, props);\n this.commitLayers(next, undefined, commitToHistory);\n return findLayer(next, id)!.layer;\n }\n\n /** Adds an empty pass-through group above the active layer (or at `placement`) and makes it active. Returns its id. */\n public addGroup(init: LayerPlacement & { name?: string; blend?: LayerBlendMode } = {}): string {\n const blend = init.blend === undefined ? 'pass-through' : (parseBlend(init.blend, true) as LayerBlendMode);\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name: init.name?.trim() || 'Group',\n visible: true,\n opacity: 1,\n blend,\n children: [],\n };\n const next = placeLayer(this.state.layers, group, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = group.id;\n this.state.activeTarget = 'pixels';\n });\n return group.id;\n }\n\n /** Puts sibling layers into a new group (one undo step). Returns the group id. */\n public groupLayers(ids: string[], name?: string): string {\n const res = groupLayersOp(this.state.layers, ids, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Replaces a group by its children (one undo step). Returns the child ids, bottom to top. */\n public ungroupLayer(id: string): string[] {\n const res = ungroupLayerOp(this.state.layers, id);\n this.commitLayers(res.layers);\n return res.ids;\n }\n\n /**\n * Moves a pixel layer to an absolute offset (`x`, `y`) or by `dx`, `dy`, as one undo step\n * (`commitToHistory = false` for the moves of a drag). Returns the offset and document bounds.\n */\n public offsetLayer(id: string, move: { x?: number; y?: number; dx?: number; dy?: number }, commitToHistory = true): { offset: { x: number; y: number }; bounds: CropBox | null } {\n const layer = requireLayer(this.state.layers, id).layer;\n if (layer.type === 'smart') {\n // A smart object moves through its placement: x, y is where its top-left corner goes.\n const tl = layer.placement.corners[0];\n const dx = move.x !== undefined ? move.x - tl.x : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - tl.y : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n this.commitLayers(offsetSmartObject(this.state.layers, id, dx, dy, this.canvasSize), undefined, commitToHistory);\n const moved = findLayer(this.state.layers, id)!.layer as SmartObjectLayer;\n return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };\n }\n const current = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const x = move.x ?? current.x + (move.dx ?? 0);\n const y = move.y ?? current.y + (move.dy ?? 0);\n const next = offsetLayerOp(this.state.layers, id, { x, y });\n this.commitLayers(next, undefined, commitToHistory);\n return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };\n }\n\n /**\n * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or\n * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint\n * target when the layer is active.\n */\n public addLayerMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' = 'reveal-all', props: LayerMaskProps = {}): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n let mask: RasterMask;\n if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n if (layer.type === 'pixel' && (layer.offset.x || layer.offset.y)) {\n // Masks live in the layer's space (they move with it).\n mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };\n }\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const next = setLayerMaskOp(this.state.layers, id, mask);\n this.commitLayers(next, () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'mask';\n });\n return mask;\n }\n\n /** Removes a layer's mask (one undo step). */\n public removeLayerMask(id: string): void {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, null), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */\n public setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory = true): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n const mask = withMaskProps(layer.mask, props);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, mask), undefined, commitToHistory);\n return mask;\n }\n\n /**\n * Bakes a pixel layer's mask into its pixels (alpha × coverage, including density, feather or\n * the depth map) and removes the mask, as one undo step.\n */\n public async applyLayerMask(id: string): Promise<void> {\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Only a pixel layer's mask can be applied to its pixels; \"${id}\" is a ${layer.type} layer. Remove the mask or keep it.`);\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n requireUnlocked(layer, 'pixels', 'apply its mask');\n const { width, height } = this.naturalSize;\n await this.renderer.prepareLayerAssets([layer], width, height);\n const sources = this.renderer.compositorSources(width, height);\n const surface = this.surfaceOf(layer);\n let out = surface;\n const b = surface.bounds;\n if (b) {\n const draft = new SurfaceDraft(surface);\n for (const t of tilesInRect(b)) {\n const data = draft.peek(t.tx, t.ty);\n if (!data) continue;\n const r = { x: t.tx * TILE + layer.offset.x, y: t.ty * TILE + layer.offset.y, width: TILE, height: TILE };\n const cov = this.compositor.maskCoverage(layer, r, sources);\n if (!cov) continue;\n const d = draft.tile(t.tx, t.ty);\n for (let p = 0, i = 3; p < cov.length; p++, i += 4) d[i] = Math.round(d[i] * cov[p]);\n }\n out = draft.commit();\n }\n const next: PixelLayer = { ...layer, surface: out };\n delete next.mask;\n delete next.source;\n this.commitLayers(mapLayer(this.state.layers, id, () => next), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /**\n * Layer via Copy (or via Cut): a new pixel layer with the selected pixels of a pixel layer (the\n * active one by default), directly above it, as one undo step. Returns the new id.\n */\n public layerViaCopy(options: { layerId?: string; cut?: boolean; name?: string } = {}): string {\n const id = options.layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError('No layer given and none is active; pass layerId (see list_layers).');\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer via copy needs a pixel layer; \"${id}\" is a ${layer.type} layer.`);\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select the pixels to copy first.');\n if (options.cut) requireUnlocked(layer, 'pixels', 'cut from it');\n const surface = this.surfaceOf(layer);\n const sb = sel.bounds;\n const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, src);\n const kept = options.cut ? new Uint8ClampedArray(px) : null;\n for (let y = 0; y < sb.height; y++) {\n for (let x = 0; x < sb.width; x++) {\n const c = sel.data[(sb.y + y) * sel.width + sb.x + x];\n const i = (y * sb.width + x) * 4 + 3;\n if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));\n px[i] = Math.round(px[i] * (c / 255));\n }\n }\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: options.name?.trim() || `${layer.name || 'Layer'} ${options.cut ? 'cut' : 'copy'}`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),\n offset: { ...layer.offset },\n };\n let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);\n if (kept) {\n const cutLayer: PixelLayer = { ...layer, surface: writeRegion(surface, src, kept) };\n delete cutLayer.source;\n next = mapLayer(next, id, () => cutLayer);\n }\n this.commitLayers(next, () => {\n this.state.activeLayerId = copy.id;\n this.state.activeTarget = 'pixels';\n });\n return copy.id;\n }\n\n // --- Free Transform ---\n\n private openTransform: FreeTransformSession | null = null;\n\n /** The Free Transform in progress (see `beginFreeTransform`), or `null`. */\n public getFreeTransform(): FreeTransformSession | null {\n if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;\n return this.openTransform;\n }\n\n private transformHost(): FreeTransformHost {\n return {\n layers: () => this.state.layers,\n activeLayerId: () => this.state.activeLayerId,\n activeTarget: () => this.state.activeTarget,\n selection: () => (this.state.selection?.bounds ? this.state.selection : null),\n canvas: () => this.naturalSize,\n pixelsOf: (layer) => this.surfaceOf(layer),\n capture: () => this.captureHistoryEntry(),\n show: (layers, selection, active) => {\n this.state.layers = layers;\n this.state.selection = selection;\n if (active !== this.state.activeLayerId) {\n this.state.activeLayerId = active;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n },\n record: (before) => {\n if (this.isApplyingHistory) return;\n this.undoStack.push(before as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n this.notify();\n },\n restore: (before) => {\n const e = before as HistoryEntry;\n this.state.layers = e.layers;\n this.state.selection = e.selection;\n this.state.activeLayerId = e.activeLayerId;\n this.state.activeTarget = e.activeTarget;\n this.notify();\n },\n };\n }\n\n /**\n * Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer\n * via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected\n * pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with\n * `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.\n * Refused while another transform is open.\n */\n public beginFreeTransform(init: FreeTransformInit = {}): FreeTransformSession {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is already open; commit or cancel it first.');\n const session = new FreeTransformSession(this.transformHost(), init);\n this.openTransform = session;\n this.notify();\n return session;\n }\n\n /**\n * Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners\n * from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:\n * distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and\n * commits it as one undo step. Returns what changed and the new bounds.\n */\n public transformLayer(spec: FreeTransformInit & { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams; warp?: WarpMesh | WarpPreset | null }): FreeTransformResult {\n const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== undefined).length;\n if (given > 1) throw new LayerOperationError('Give only one of matrix, corners and params.');\n if (!given && spec.warp === undefined) throw new LayerOperationError('Give matrix, corners or params (or a warp).');\n if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {\n throw new LayerOperationError('matrix must be six finite numbers [a, b, c, d, e, f] (x\\' = a·x + c·y + e, y\\' = b·x + d·y + f).');\n }\n if (spec.matrix) {\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the layer to a line); its determinant a·d − b·c must not be 0.');\n }\n const session = this.beginFreeTransform(spec);\n try {\n // Relative to where the content is now (a smart object's current placement).\n const g = session.startGeometry;\n const corners = spec.matrix ? (g.corners.map((p) => applyMatrix(spec.matrix!, p)) as unknown as Quad) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;\n let warp: WarpMesh | null = spec.warp === undefined ? (g.warp ?? null) : null;\n if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(session.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points, row by row.');\n warp = spec.warp;\n }\n }\n session.update({ corners, warp }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */\n public flipLayer(axis: 'horizontal' | 'vertical', init: FreeTransformInit = {}): FreeTransformResult {\n if (axis !== 'horizontal' && axis !== 'vertical') throw new LayerOperationError(`axis must be horizontal or vertical, got \"${String(axis)}\".`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n session.update({ corners: flipCorners(session.startGeometry, axis) }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /**\n * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's\n * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).\n */\n public rotateLayer90(turn: 90 | -90 | 180, init: FreeTransformInit = {}): FreeTransformResult {\n if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n // The frame in the document (a smart object's placed outline).\n const b = session.smartObject ? (transformedBox(session.startGeometry) ?? session.box) : session.box;\n // About the centre, rounded so an odd-by-even box still lands on the pixel grid.\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);\n const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);\n const m: AffineMatrix =\n turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];\n session.update({ corners: session.startGeometry.corners.map((p) => applyMatrix(m, p)) as unknown as Quad }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */\n public addLayerFilter(id: string, filter: Partial<LayerFilter> & { kind: LayerFilter['kind'] }): LayerFilter[] {\n const f = normalizeFilter(filter);\n const next = addLayerFilterOp(this.state.layers, id, f);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */\n public updateLayerFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): LayerFilter[] {\n const next = updateLayerFilterOp(this.state.layers, id, filterId, changes);\n this.commitLayers(next, undefined, commitToHistory);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Removes one filter (one undo step). Returns the remaining filters. */\n public removeLayerFilter(id: string, filterId: string): LayerFilter[] {\n const next = removeLayerFilterOp(this.state.layers, id, filterId);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n // --- Smart objects ---\n\n private get canvasSize(): { width: number; height: number } {\n return this.naturalSize;\n }\n\n /**\n * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the\n * active one) become one smart object at the topmost one's position. A single pixel layer keeps\n * its id and properties and its filters become smart filters (it renders exactly as before);\n * anything else nests as a layer document. Returns the smart object's id.\n */\n public convertToSmartObject(ids?: string[], options: { name?: string } = {}): string {\n const list = ids?.length ? ids : this.state.activeLayerId ? [this.state.activeLayerId] : [];\n if (!list.length) throw new LayerOperationError('No layers given and none is active; pass layerIds (see list_layers).');\n const res = convertToSmartObjectOp(this.state.layers, list, this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l), name: options.name });\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Layer › Smart Objects › Rasterize (one undo step): the smart object becomes a pixel layer holding what it renders. */\n public rasterizeSmartObject(id: string): string {\n const res = rasterizeSmartObjectOp(this.state.layers, id);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId === id && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n });\n return id;\n }\n\n /** A smart object's contents, placement and smart filters as plain data (see `SmartObjectInfo`). */\n public getSmartObject(id: string): SmartObjectInfo {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const g = placementGeometry(layer);\n const readout = describeTransform(g);\n const stack = layer.smartFilters;\n return {\n layerId: layer.id,\n contents: {\n id: c.id,\n name: c.name,\n kind: c.kind,\n width: c.width,\n height: c.height,\n decoded: isContentsDecoded(c),\n fileType: c.file?.type ?? null,\n linkedPath: c.linked?.path ?? null,\n instances: instancesOf(this.state.layers, c.id),\n layerCount: c.document ? flattenLayers(c.document.layers).length : 0,\n },\n placement: {\n corners: g.corners.map((p) => ({ x: p.x, y: p.y })) as unknown as Quad,\n warp: layer.placement.warp ?? null,\n interpolation: layer.placement.interpolation ?? 'bicubic',\n affine: readout.affine,\n scaleX: readout.scaleX,\n scaleY: readout.scaleY,\n rotate: readout.rotate,\n skewX: readout.skewX,\n center: anchorPoint(g, 'center'),\n bounds: transformedBox(g),\n },\n smartFilters: {\n enabled: stack?.enabled ?? true,\n filters: (stack?.filters ?? []).map((f) => (f.kind === 'psd' ? { id: f.id, kind: 'psd', name: f.name, enabled: f.enabled, blend: f.blend, opacity: f.opacity } : { ...f })),\n mask: stack?.mask\n ? { enabled: stack.mask.enabled !== false, linked: stack.mask.linked !== false, density: stack.mask.density ?? 1, feather: stack.mask.feather ?? 0, bounds: contentBounds(stack.mask.surface), defaultValue: stack.mask.defaultValue }\n : null,\n },\n bounds: this.getLayerBounds(id),\n };\n }\n\n /**\n * Sets a smart object's placement (one undo step, or none with `commitToHistory = false` during a\n * drag): where the contents' corners land, an optional warp in contents pixels and the\n * interpolation. The contents render again from the source; a linked mask follows a warp-free\n * change. Returns the new placement summary.\n */\n public setSmartObjectPlacement(id: string, placement: SmartPlacement, commitToHistory = true): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const res = setSmartPlacement(this.state.layers, id, placement, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return { ...this.getSmartObject(id).placement, masksMoved: res.masksMoved };\n }\n\n /**\n * Sets a smart object's transform absolutely (one undo step): exactly one of `corners` (where the\n * contents' corners land), `matrix` (contents pixels → document) or the numbers (`scaleX`,\n * `scaleY` in percent of the contents' own size, `rotate`, `skewX`, `skewY`, and `x`, `y` where\n * the contents' centre goes, default where it is now); plus, optionally, a warp (a preset, a mesh\n * in contents pixels, or `null` to remove it) and the interpolation. Unlike `transformLayer`,\n * nothing is relative to the current transform.\n */\n public setSmartObjectTransform(\n id: string,\n spec: {\n corners?: Quad;\n matrix?: AffineMatrix;\n scaleX?: number;\n scaleY?: number;\n rotate?: number;\n skewX?: number;\n skewY?: number;\n x?: number;\n y?: number;\n warp?: WarpMesh | WarpPreset | null;\n interpolation?: Interpolation;\n }\n ): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const layer = requireSmartObject(this.state.layers, id);\n const g = placementGeometry(layer);\n const numeric = (['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y'] as const).filter((k) => spec[k] !== undefined);\n const ways = [spec.corners !== undefined, spec.matrix !== undefined, numeric.length > 0].filter(Boolean).length;\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: corners, matrix, or the numbers (scaleX, scaleY, rotate, skewX, skewY, x, y).');\n let corners: Quad = g.corners;\n if (spec.corners) corners = spec.corners;\n else if (spec.matrix) {\n if (!(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) throw new LayerOperationError(\"matrix must be six finite numbers [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\");\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the contents to a line); a·d − b·c must not be 0.');\n corners = cornersFromMatrix(g.box, spec.matrix);\n } else if (numeric.length) {\n if (spec.scaleX === 0 || spec.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the contents to a line).');\n for (const k of numeric) if (!Number.isFinite(spec[k] as number)) throw new LayerOperationError(`${k} must be a finite number.`);\n const now = describeTransform(g);\n const centre = anchorPoint(g, 'center');\n const params = {\n scaleX: spec.scaleX ?? (now.affine ? now.scaleX : 100),\n scaleY: spec.scaleY ?? (now.affine ? now.scaleY : 100),\n rotate: spec.rotate ?? (now.affine ? now.rotate : 0),\n skewX: spec.skewX ?? (now.affine ? now.skewX : 0),\n skewY: spec.skewY ?? 0,\n };\n const m = matrixFromParams(g.box, { ...params, reference: 'center' });\n const cx = g.box.width / 2;\n const cy = g.box.height / 2;\n const to = { x: spec.x ?? centre.x, y: spec.y ?? centre.y };\n corners = cornersFromMatrix(g.box, multiplyMatrices(m, translateMatrix(to.x - cx, to.y - cy)));\n }\n let warp: WarpMesh | null | undefined = layer.placement.warp ?? null;\n if (spec.warp === null) warp = null;\n else if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(g.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points in contents pixels, row by row.');\n warp = spec.warp;\n }\n }\n const next: SmartPlacement = { corners, warp, interpolation: spec.interpolation ?? layer.placement.interpolation };\n return this.setSmartObjectPlacement(id, next);\n }\n\n /**\n * Layer › Smart Objects › Replace Contents (one undo step): new contents for the smart object and\n * every instance linked to it, from RGBA pixels, PSD bytes (a layer document) or contents. Each\n * instance keeps its scale and rotation about the contents' centre. Returns the contents id and\n * the layers that changed.\n */\n public replaceSmartObjectContents(\n id: string,\n source: { pixels: PixelBuffer; name?: string } | { psd: Uint8Array; name?: string } | { contents: SmartContents }\n ): { contentsId: string; width: number; height: number; layerIds: string[] } {\n const layer = requireSmartObject(this.state.layers, id);\n let contents: SmartContents;\n if ('contents' in source) contents = source.contents;\n else if ('pixels' in source) contents = rasterContents(source.pixels, source.name ?? layer.contents.name.replace(/\\.\\w+$/, '.png'));\n else {\n const psd = importPsd(source.psd, { newId: () => newLayerId() });\n contents = documentContents({ layers: psd.layers, globalLight: psd.globalLight, patterns: psd.patterns.length ? psd.patterns : undefined }, psd.width, psd.height, source.name ?? `${layer.name || 'Contents'}.psb`);\n contents.file = { bytes: source.psd.slice(), type: psd.info.psb ? 'psb' : 'psd' };\n }\n const res = replaceContentsOp(this.state.layers, layer.contents.id, contents, this.canvasSize);\n this.commitLayers(res.layers);\n return { contentsId: layer.contents.id, width: contents.width, height: contents.height, layerIds: res.ids };\n }\n\n /** `replaceSmartObjectContents` from an image (PNG data URL or base64 decodes anywhere; other formats need a browser). */\n public async replaceSmartObjectContentsFromImage(id: string, image: string, name?: string): Promise<{ contentsId: string; width: number; height: number; layerIds: string[] }> {\n const pixels = await this.decodeImage(image);\n return this.replaceSmartObjectContents(id, { pixels, name });\n }\n\n /**\n * Layer › Smart Objects › Export Contents: the file the contents came from while unedited, else a\n * PNG (raster contents) or a PSD / PSB of the nested document. Returns the file name, MIME type\n * and bytes.\n */\n public exportSmartObjectContents(id: string): { name: string; mimeType: string; bytes: Uint8Array } {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const base = c.name.replace(/\\.\\w+$/, '') || 'Contents';\n if (c.file) {\n const mime: Record<string, string> = { psd: 'image/vnd.adobe.photoshop', psb: 'image/vnd.adobe.photoshop', 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 (layer.type === 'pixel') return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };\n if (layer.type === 'annotation') {\n const b = getAnnotationBounds(layer.annotation);\n const pad = 4 + ('strokeWidth' in layer.annotation ? (layer.annotation as { strokeWidth: number }).strokeWidth : 0);\n return coveringRect({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });\n }\n if (layer.type === 'group') {\n let r: CropBox | null = null;\n for (const c of layer.children) r = unionRect(r, this.paintBounds(c));\n return r ?? canvas;\n }\n return canvas;\n }\n\n /**\n * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,\n * opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.\n * Returns the merged id.\n */\n public async mergeDown(id: string): Promise<string> {\n const loc = requireLayer(this.state.layers, id);\n const upper = loc.layer;\n const siblings = loc.parent ? loc.parent.children : this.state.layers;\n const lower = siblings[loc.index - 1];\n if (!lower) throw new LayerOperationError(`Layer \"${upper.name || id}\" is the bottom of its group; there is nothing below to merge into.`);\n if (lower.type !== 'pixel') throw new LayerOperationError(`Merge Down needs a pixel layer below; \"${lower.name || lower.id}\" is a ${lower.type} layer. Move the layer or merge into another one.`);\n if (!upper.visible) throw new LayerOperationError(`Layer \"${upper.name || id}\" is hidden; show it or delete it instead of merging.`);\n requireUnlocked(upper, 'all', 'merge it');\n requireUnlocked(lower, 'pixels', 'merge into it');\n let rect = unionRect(this.paintBounds(lower), upper.type === 'adjustment' ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };\n // Effects paint beyond the layers' pixels: merge as far as they reach.\n const reach = Math.max(effectsMargin(lower.effects), upper.type === 'adjustment' ? 0 : effectsMargin(upper.effects));\n if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };\n const base: PixelLayer = { ...lower, blend: 'normal', opacity: 1 };\n delete base.clipped;\n const surface = await this.compositeToSurface([base, upper], rect);\n const merged: PixelLayer = { ...lower, surface, offset: { x: 0, y: 0 } };\n delete merged.mask;\n delete merged.fillOpacity;\n delete merged.filters;\n delete merged.source;\n delete merged.effects;\n let next = mapLayer(this.state.layers, id, () => null);\n next = mapLayer(next, lower.id, () => merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n });\n return merged.id;\n }\n\n /**\n * Merge Visible: the visible top-level layers become one normal pixel layer (at the position of\n * the lowest of them); hidden layers stay. One undo step. Returns the merged id.\n */\n public async mergeVisible(): Promise<string> {\n const visible = this.state.layers.filter((l) => l.visible);\n if (!visible.length) throw new LayerOperationError('No layer is visible.');\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(visible, { x: 0, y: 0, width, height });\n const merged: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: 'Merged',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n const at = this.state.layers.indexOf(visible[0]);\n const next = this.state.layers.filter((l) => !l.visible);\n next.splice(this.state.layers.slice(0, at).filter((l) => !l.visible).length, 0, merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return merged.id;\n }\n\n /** Flatten Image: every visible layer composited into one Background layer; hidden layers are discarded. One undo step. */\n public async flattenImage(): Promise<string> {\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(this.state.layers, { x: 0, y: 0, width, height });\n const bg: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n this.commitLayers([bg], () => {\n this.state.activeLayerId = bg.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return bg.id;\n }\n\n private thumbCache = new WeakMap<object, Map<string, PixelBuffer>>();\n\n /**\n * A small RGBA preview of a pixel layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px, point-sampled; `null` for layers without pixels.\n */\n public getLayerThumbnail(id: string, options: { maxSide?: number; mask?: boolean } = {}): PixelBuffer | null {\n const layer = this.getLayer(id);\n if (!layer) return null;\n const { width, height } = this.naturalSize;\n const side = Math.max(4, Math.min(256, Math.round(options.maxSide ?? 48)));\n const k = Math.min(1, side / Math.max(width, height));\n const tw = Math.max(1, Math.round(width * k));\n const th = Math.max(1, Math.round(height * k));\n const surface = options.mask ? (layer.mask && !layer.mask.source ? layer.mask.surface : null) : layer.type === 'pixel' || layer.type === 'smart' ? this.surfaceOf(layer) : null;\n if (!surface) return null;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const key = `${tw}x${th}:${ox},${oy}`;\n let byKey = this.thumbCache.get(surface);\n const hit = byKey?.get(key);\n if (hit) return hit;\n const out = new Uint8ClampedArray(tw * th * 4);\n for (let y = 0; y < th; y++) {\n const dy = Math.min(height - 1, Math.floor(((y + 0.5) * height) / th)) - oy;\n for (let x = 0; x < tw; x++) {\n const dx = Math.min(width - 1, Math.floor(((x + 0.5) * width) / tw)) - ox;\n const v = readPixel(surface, dx, dy);\n const i = (y * tw + x) * 4;\n if (surface.channels === 1) {\n out[i] = out[i + 1] = out[i + 2] = v[0];\n out[i + 3] = 255;\n } else out.set(v, i);\n }\n }\n const thumb = { width: tw, height: th, data: out };\n if (!byKey) {\n byKey = new Map();\n this.thumbCache.set(surface, byKey);\n }\n byKey.set(key, thumb);\n return thumb;\n }\n\n /**\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'>> & { brush?: Partial<BrushSettings>; mixer?: Partial<MixerSettings> }\n ): PaintOptions {\n const cur = this.state.paint;\n const next: PaintOptions = {\n brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,\n foreground: options.foreground !== undefined ? toHex(parseColor(options.foreground)) : cur.foreground,\n background: options.background !== undefined ? toHex(parseColor(options.background)) : cur.background,\n mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,\n mixerLoad: cur.mixerLoad,\n };\n this.state.paint = next;\n this.notify();\n return next;\n }\n\n /**\n * Loads the Mixer Brush's reservoir full with `color` (default the foreground); the picked-up\n * colour is kept. View state, no undo. Returns the brush load.\n */\n public loadMixerBrush(color?: string | number[]): MixerLoad {\n const c = parseColor(color ?? this.state.paint.foreground);\n const load = loadMixer(this.state.paint.mixerLoad, [c[0] / 255, c[1] / 255, c[2] / 255]);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Cleans the Mixer Brush: empties the pickup well (the reservoir is kept). Returns the brush load. */\n public cleanMixerBrush(): MixerLoad {\n const load = cleanMixer(this.state.paint.mixerLoad);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Empties the Mixer Brush entirely (no reservoir, nothing picked up). Returns the brush load. */\n public emptyMixerBrush(): MixerLoad {\n this.state.paint = { ...this.state.paint, mixerLoad: EMPTY_MIXER_LOAD };\n this.notify();\n return EMPTY_MIXER_LOAD;\n }\n\n /** D: black foreground, white background. */\n public resetPaintColors(): void {\n this.setPaintOptions({ foreground: '#000000', background: '#ffffff' });\n }\n\n /** X: swaps the foreground and background colours. */\n public swapPaintColors(): void {\n const { foreground, background } = this.state.paint;\n this.setPaintOptions({ foreground: background, background: foreground });\n }\n\n /** Resolves and validates what a stroke, fill or clear paints into. */\n private paintTarget(layerId: string | undefined, channel: LayerTarget | undefined, action: string) {\n const id = layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`);\n const layer = requireLayer(this.state.layers, id).layer;\n const ch: LayerTarget = channel ?? (id === this.state.activeLayerId ? this.state.activeTarget : 'pixels');\n if (ch === 'mask') {\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);\n if (layer.mask.source) throw new LayerOperationError(`Layer \"${layer.name || id}\" has a depth mask, which is generated, not painted.`);\n requireUnlocked(layer, 'all', action);\n } else if (ch === 'filter-mask') {\n if (layer.type !== 'smart' || !layer.smartFilters?.mask) {\n throw new LayerOperationError(`Layer \"${layer.name || id}\" has no smart filter mask to ${action}; add one with set_smart_filter_mask, or target pixels or mask.`);\n }\n if (!isContentsDecoded(layer.contents)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not decoded here, so its filter mask cannot be painted; run prepareSmartObjects() first.`);\n requireUnlocked(layer, 'all', action);\n } else {\n if (layer.type === 'smart') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);\n requireUnlocked(layer, 'pixels', action);\n }\n if (!layer.visible) throw new LayerOperationError(`Layer \"${layer.name || id}\" is hidden; show it before you ${action}.`);\n const offset = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const surface = ch === 'mask' ? layer.mask!.surface : ch === 'filter-mask' ? (layer as SmartObjectLayer).smartFilters!.mask!.surface : this.surfaceOf(layer as PixelLayer);\n const lockTransparency = ch === 'pixels' && Boolean(layer.locks?.transparency);\n return { id, layer, channel: ch, offset, surface, lockTransparency };\n }\n\n /** The layer with its pixels (or mask) replaced by `surface`. */\n private withSurface(layer: ImageLayer, channel: LayerTarget, surface: TiledSurface): ImageLayer {\n if (channel === 'mask') return { ...layer, mask: { ...layer.mask!, surface } } as ImageLayer;\n if (channel === 'filter-mask') return withFilterMaskSurface(layer as SmartObjectLayer, surface, this.canvasSize);\n const next = { ...(layer as PixelLayer), surface };\n delete next.source;\n return next;\n }\n\n /**\n * Starts a brush or eraser stroke on a layer's pixels or mask (default: the active layer and\n * target, the paint options' brush and colours). Feed points with `push`, finish with `end()`\n * (one undo step for the whole stroke) or `cancel()`. While it runs, the stack shows the stroke\n * without recording history.\n */\n public beginStroke(init: StrokeInit = {}): PaintStrokeSession {\n const tool = init.tool ?? 'brush';\n if (tool !== 'brush' && tool !== 'eraser' && tool !== 'mixer') throw new LayerOperationError(`Unknown paint tool \"${String(tool)}\"; use brush, eraser or mixer.`);\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : 'paint');\n if (tool === 'mixer' && target.channel !== 'pixels') {\n throw new LayerOperationError('The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.');\n }\n const brush = normalizeBrush({ ...this.state.paint.brush, ...(init.brush ?? {}) });\n const color = parseColor(init.color ?? this.state.paint.foreground);\n const background = parseColor(init.background ?? this.state.paint.background);\n const toMaskGrey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode: tool === 'eraser' ? 'erase' : 'paint',\n color: target.channel !== 'pixels' ? toMaskGrey(color) : color,\n background: target.channel !== 'pixels' ? toMaskGrey(background) : background,\n opacity: brush.opacity,\n blend: target.channel !== 'pixels' ? 'normal' : brush.blend,\n lockTransparency: target.lockTransparency,\n selection: this.state.selection?.bounds ? this.state.selection : null,\n offset: target.offset,\n };\n if (tool === 'mixer') return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */\n private mixerStroke(init: StrokeInit, brush: BrushSettings): MixerStrokeInit {\n const settings = normalizeMixer({ ...this.state.paint.mixer, ...(init.mixer ?? {}) });\n let load = this.state.paint.mixerLoad;\n if (init.color !== undefined || settings.loadEachStroke) {\n const c = parseColor(init.color ?? this.state.paint.foreground);\n load = loadMixer(load, [c[0] / 255, c[1] / 255, c[2] / 255]);\n }\n return {\n create: (pixels) => new MixerStroke(brush, settings, load, pixels),\n end: (after) => {\n const next = settings.cleanEachStroke ? cleanMixer(after) : after;\n this.state.paint = { ...this.state.paint, mixerLoad: next };\n return next;\n },\n };\n }\n\n /** @internal Shows a stroke's live surface (no history). */\n public _strokeUpdate(layerId: string, channel: LayerTarget, surface: TiledSurface, rects: CropBox[]): void {\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n // A filter mask re-renders the smart object (and feathering spreads the change): redraw it all.\n this.notify(channel === 'filter-mask' ? undefined : rects);\n }\n\n /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */\n public _strokeCommit(before: HistoryEntryHandle, layerId: string, channel: LayerTarget, surface: TiledSurface | null): void {\n if (surface) this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n else this.state.layers = (before as unknown as HistoryEntry).layers;\n if (surface && !this.isApplyingHistory) {\n this.undoStack.push(before as unknown as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n this.notify();\n }\n\n /** @internal The pre-stroke snapshot. */\n public _captureForStroke(): HistoryEntryHandle {\n return this.captureHistoryEntry() as unknown as HistoryEntryHandle;\n }\n\n /**\n * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional\n * pressure, through the brush, eraser or Mixer Brush. One undo step. Returns the layer, channel, the changed\n * document bounds and the number of dabs.\n */\n public paintStroke(init: StrokeInit & { points: StrokePoint[] }): PaintStrokeResult {\n if (!Array.isArray(init.points) || !init.points.length) throw new LayerOperationError('points must hold at least one { x, y } point.');\n for (const p of init.points) {\n if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) throw new LayerOperationError('Every point needs finite x and y (document pixels).');\n }\n const session = this.beginStroke(init);\n session.push(init.points);\n return session.end();\n }\n\n /**\n * Edit › Fill: fills the selection (or, without one or with `useSelection: false`, the whole\n * canvas) on a layer's pixels or mask with a colour (default the foreground), at `opacity` with\n * `blend`. One undo step. Returns the changed bounds.\n */\n public fillPixels(options: FillOptions = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'paint');\n }\n\n /**\n * Edit › Clear: removes the selected pixels (alpha to 0; on a transparency-locked layer or a mask\n * it fills with the background colour). Without a selection (or with `useSelection: false`) the\n * whole canvas. One undo step. Returns the changed bounds.\n */\n public clearPixels(options: Omit<FillOptions, 'color' | 'blend'> = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'erase');\n }\n\n private fillOrClear(options: FillOptions, mode: 'paint' | 'erase'): PaintStrokeResult {\n const target = this.paintTarget(options.layerId, options.channel, mode === 'paint' ? 'fill' : 'clear');\n const { width, height } = this.naturalSize;\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const blend = (parseBlend(options.blend) as BlendMode | undefined) ?? 'normal';\n const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;\n const region = sel?.bounds ?? { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const ones = new Float32Array(region.width * region.height);\n if (sel) {\n for (let y = 0; y < region.height; y++) {\n for (let x = 0; x < region.width; x++) ones[y * region.width + x] = sel.data[(region.y + y) * sel.width + region.x + x] / 255;\n }\n } else ones.fill(1);\n coverage.accumulate(region, ones, 1);\n const color = parseColor(options.color ?? this.state.paint.foreground);\n const background = parseColor(this.state.paint.background);\n const grey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode,\n color: target.channel !== 'pixels' ? grey(color) : color,\n background: target.channel !== 'pixels' ? grey(background) : background,\n opacity,\n blend: target.channel !== 'pixels' ? 'normal' : blend,\n lockTransparency: target.lockTransparency,\n selection: null,\n offset: target.offset,\n };\n const surface = paintCoverageInto(target.surface, coverage, apply, [region]);\n const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));\n this.commitLayers(next);\n return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };\n }\n\n // --- Adjustments ---\n\n /** The pixel layer the base sliders edit: the active pixel layer, else the Background. */\n private developLayer(): PixelLayer | null {\n return developTarget(this.state.layers, this.state.activeLayerId);\n }\n\n /**\n * Sets base adjustment values as one undo step (unless `commitToHistory` is false). The base\n * sliders edit the `develop` filter of the active pixel layer (or the Background), `blur` its\n * blur filter (or `legacyOutputBlur` in migrated documents), `opacity` the output opacity, and\n * `layers` replaces the adjustment layers.\n */\n public setAdjustments(partial: Partial<ImageAdjustments>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const target = this.developLayer();\n let layers = this.state.layers;\n if (target && (DEVELOP_KEYS.some((k) => k in partial) || 'process' in partial)) {\n layers = mapLayer(layers, target.id, (l) => withDevelop(l as PixelLayer, partial));\n }\n if ('blur' in partial && typeof partial.blur === 'number') {\n const blur = Math.max(0, partial.blur);\n if (this.state.legacyOutputBlur !== undefined) this.state.legacyOutputBlur = blur;\n else if (target) layers = mapLayer(layers, target.id, (l) => withBlur(l as PixelLayer, blur));\n }\n if ('opacity' in partial && typeof partial.opacity === 'number') {\n this.state.outputOpacity = Math.min(1, Math.max(0, partial.opacity));\n }\n if ('layers' in partial) {\n layers = this.replaceAdjustmentNodes(layers, normalizeAdjustmentLayers(partial.layers));\n }\n this.state.layers = layers;\n this.notify();\n }\n\n /** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */\n public get isProcessOutdated(): boolean {\n this.syncViews();\n return normalizeProcessVersion(this.state.adjustments.process) < LATEST_PROCESS_VERSION;\n }\n\n /**\n * Update to Current Process: re-interprets the current adjustment values with the latest\n * rendering process (one undo step). Returns false when already current.\n */\n public upgradeProcess(): boolean {\n if (!this.isProcessOutdated) return false;\n this.setAdjustments({ process: LATEST_PROCESS_VERSION });\n return true;\n }\n\n /** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */\n public resetAdjustments(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) =>\n withBlur(withDevelop(l as PixelLayer, { ...DEFAULT_ADJUSTMENTS, process: LATEST_PROCESS_VERSION }), 0)\n );\n }\n delete this.state.legacyOutputBlur;\n this.state.outputOpacity = 1;\n this.notify();\n }\n\n // --- Adjustment layers ---\n\n /** The adjustment layers in stack order, bottom (first) to top (last). */\n public getAdjustmentLayers(): AdjustmentLayer[] {\n this.syncViews();\n return this.state.adjustments.layers ?? [];\n }\n\n public getAdjustmentLayer(id: string): AdjustmentLayer | undefined {\n return this.getAdjustmentLayers().find((l) => l.id === id);\n }\n\n private adjustmentNodes(layers: readonly ImageLayer[] = this.state.layers): AdjustmentLayerNode[] {\n return flattenLayers(layers).filter((l): l is AdjustmentLayerNode => l.type === 'adjustment');\n }\n\n /**\n * Where a new layer goes when no position is given: directly above the topmost layer that is\n * not an annotation, so it adjusts what adjustment layers always adjusted (annotations stay on top).\n */\n private defaultInsertIndex(layers: readonly ImageLayer[] = this.state.layers): number {\n let i = layers.length;\n while (i > 0 && layers[i - 1].type === 'annotation') i--;\n return i;\n }\n\n /** Inserts `node` by `aboveId` / `parentId` / adjustment index / default position. */\n private placeLayer(\n layers: ImageLayer[],\n node: ImageLayer,\n where: { aboveId?: string; parentId?: string; adjustmentIndex?: number }\n ): ImageLayer[] {\n if (where.aboveId) {\n const loc = findLayer(layers, where.aboveId);\n if (loc) return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n if (where.parentId) {\n const loc = findLayer(layers, where.parentId);\n if (loc && loc.layer.type === 'group') return insertLayer(layers, node, loc.layer.id);\n }\n if (where.adjustmentIndex !== undefined) {\n const nodes = this.adjustmentNodes(layers);\n const at = Math.max(0, Math.min(nodes.length, Math.round(where.adjustmentIndex)));\n if (at < nodes.length) {\n const loc = findLayer(layers, nodes[at].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index);\n }\n if (nodes.length) {\n const loc = findLayer(layers, nodes[nodes.length - 1].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n }\n return insertLayer(layers, node, null, this.defaultInsertIndex(layers));\n }\n\n /** `setAdjustments({ layers })`: updates nodes by id in place, removes missing ones, adds new ones. */\n private replaceAdjustmentNodes(layers: ImageLayer[], next: AdjustmentLayer[]): ImageLayer[] {\n const { width, height } = this.naturalSize;\n const byId = new Map(next.map((l) => [l.id, l]));\n let out = layers;\n for (const node of this.adjustmentNodes(layers)) {\n const update = byId.get(node.id);\n out = mapLayer(out, node.id, () => (update ? mergeAdjustmentNode(node, update, width, height) : null));\n }\n const existing = new Set(this.adjustmentNodes(out).map((n) => n.id));\n for (const l of next) {\n if (existing.has(l.id)) continue;\n out = this.placeLayer(out, adjustmentNodeFromLegacy(l, width, height), { adjustmentIndex: Infinity });\n }\n // Keep the given order among the adjustment layers.\n const order = next.map((l) => l.id);\n for (let to = 0; to < order.length; to++) out = this.moveAdjustmentNode(out, order[to], to);\n return out;\n }\n\n /**\n * Adds an adjustment layer and returns its id, as one undo step. With no position it goes\n * above the other adjustment layers (or, without any, above the topmost non-annotation layer,\n * so annotations stay on top); `index` counts adjustment layers (0 = below the others);\n * `aboveId` / `parentId` place it anywhere in the stack. `kind` alone gives neutral parameters.\n */\n public addAdjustmentLayer<K extends AdjustmentKind>(init: K | AdjustmentLayerInit<K>, index?: number): string {\n const spec: AdjustmentLayerInit<K> = typeof init === 'string' ? { kind: init } : init;\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer = createAdjustmentLayer(spec.kind, {\n id: spec.id && !ids.has(spec.id) ? spec.id : undefined,\n name: spec.name,\n params: spec.params,\n enabled: spec.enabled,\n opacity: spec.opacity,\n blend: spec.blend,\n mask: spec.mask ?? undefined,\n });\n while (ids.has(layer.id)) layer.id = newLayerId('adj');\n const { width, height } = this.naturalSize;\n this.recordHistory();\n const node = adjustmentNodeFromLegacy(layer, width, height);\n const positioned = Boolean(spec.aboveId || spec.parentId);\n this.state.layers = this.placeLayer(this.state.layers, node, {\n aboveId: spec.aboveId,\n parentId: spec.parentId,\n adjustmentIndex: index !== undefined ? index : positioned ? undefined : Infinity,\n });\n this.notify();\n return layer.id;\n }\n\n /**\n * Updates a layer (`params` merged into the current ones, then normalised). Pass\n * `commitToHistory = false` for the moves of a drag after recording its start once.\n * Returns false for an unknown id.\n */\n public updateAdjustmentLayer(id: string, update: AdjustmentLayerUpdate, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n const { width, height } = this.naturalSize;\n const node = loc.layer;\n const merged = normalizeAdjustmentLayer({\n id: node.id,\n kind: node.kind,\n enabled: update.enabled ?? node.visible,\n opacity: update.opacity ?? node.opacity,\n blend: update.blend ?? node.blend,\n name: update.name ?? node.name,\n params: { ...node.params, ...(update.params ?? {}) },\n });\n if (!merged) return false;\n if (commitToHistory) this.recordHistory();\n const next = {\n ...node,\n visible: merged.enabled,\n opacity: merged.opacity,\n blend: merged.blend,\n name: merged.name ?? '',\n params: merged.params,\n } as AdjustmentLayerNode;\n if (update.mask !== undefined) {\n const mask = update.mask ? rasterMaskFromLegacy(update.mask, width, height) : null;\n if (mask) next.mask = mask;\n else delete next.mask;\n }\n this.state.layers = mapLayer(this.state.layers, id, () => next);\n this.notify();\n return true;\n }\n\n /** Removes a layer as one undo step. Returns false for an unknown id. */\n public removeAdjustmentLayer(id: string): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, id, () => null);\n if (this.state.activeLayerId === id) this.state.activeLayerId = null;\n this.notify();\n return true;\n }\n\n private moveAdjustmentNode(layers: ImageLayer[], id: string, toIndex: number): ImageLayer[] {\n const nodes = this.adjustmentNodes(layers);\n const from = nodes.findIndex((n) => n.id === id);\n if (from === -1) return layers;\n const to = Math.max(0, Math.min(nodes.length - 1, Math.round(toIndex)));\n if (to === from) return layers;\n const node = nodes[from];\n const anchor = nodes[to];\n const without = mapLayer(layers, id, () => null);\n const loc = findLayer(without, anchor.id)!;\n return insertLayer(without, node, loc.parent?.id ?? null, to > from ? loc.index + 1 : loc.index);\n }\n\n /** Moves a layer to `toIndex` among the adjustment layers (0 = bottom) as one undo step. Returns false if nothing moved. */\n public moveAdjustmentLayer(id: string, toIndex: number): boolean {\n const next = this.moveAdjustmentNode(this.state.layers, id, toIndex);\n if (next === this.state.layers) return false;\n this.recordHistory();\n this.state.layers = next;\n this.notify();\n return true;\n }\n\n /** Shows or hides a layer (flips it without `enabled`) as one undo step. */\n public toggleAdjustmentLayer(id: string, enabled?: boolean): boolean {\n const layer = this.getAdjustmentLayer(id);\n if (!layer) return false;\n const next = enabled ?? !layer.enabled;\n if (next === layer.enabled) return false;\n return this.updateAdjustmentLayer(id, { enabled: next });\n }\n\n /** Removes every adjustment layer as one undo step. */\n public clearAdjustmentLayers(): void {\n const nodes = this.adjustmentNodes();\n if (!nodes.length) return;\n this.recordHistory();\n let layers = this.state.layers;\n for (const n of nodes) layers = mapLayer(layers, n.id, () => null);\n this.state.layers = layers;\n if (nodes.some((n) => n.id === this.state.activeLayerId)) this.state.activeLayerId = null;\n this.notify();\n }\n\n // --- Selection ---\n\n /** The active selection, or `null`. */\n public getSelection(): SelectionMask | null {\n return this.state.selection ?? null;\n }\n\n public get hasSelection(): boolean {\n return Boolean(this.state.selection?.bounds);\n }\n\n private get naturalSize(): { width: number; height: number } {\n return {\n width: Math.max(1, Math.round(this.state.imageDimensions.width)),\n height: Math.max(1, Math.round(this.state.imageDimensions.height)),\n };\n }\n\n private dropMismatchedSelection(): void {\n const sel = this.state.selection;\n if (!sel) return;\n const { width, height } = this.naturalSize;\n if (sel.width !== width || sel.height !== height) this.state.selection = null;\n }\n\n /**\n * Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a\n * different size (e.g. an AI segmentation at another resolution) is resampled to the image.\n * Returns false when nothing changed.\n */\n public setSelection(mask: SelectionMask | null, mode: SelectionMode = 'replace'): boolean {\n const { width, height } = this.naturalSize;\n let next: SelectionMask | null = null;\n if (mask) {\n const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);\n next = combineSelections(this.state.selection, sized, mode);\n } else if (mode !== 'replace') {\n return false;\n }\n const prev = normalizeSelection(this.state.selection ?? null);\n if (prev === next || (prev && next && sameSelection(prev, next))) return false;\n this.recordHistory();\n this.state.selection = next;\n this.notify();\n return true;\n }\n\n /** Select › All. */\n public selectAll(): boolean {\n const { width, height } = this.naturalSize;\n return this.setSelection(fullSelection(width, height));\n }\n\n /** Select › Deselect. */\n public deselect(): boolean {\n return this.setSelection(null);\n }\n\n /** Select › Inverse (with nothing selected, selects everything). */\n public invertSelection(): boolean {\n const { width, height } = this.naturalSize;\n const current = this.state.selection ?? createSelectionMask(width, height);\n return this.setSelection(invertSelectionMask(current));\n }\n\n private shapeCommand(mask: SelectionMask, options: SelectionCommandOptions): boolean {\n const shaped = options.feather && options.feather > 0 ? modifySelectionMask(mask, 'feather', options.feather) : mask;\n return this.setSelection(shaped, options.mode ?? 'replace');\n }\n\n /** Rectangular marquee in natural image pixels. */\n public selectRect(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(rectSelection(width, height, rect, options), options);\n }\n\n /** Elliptical marquee inscribed in `rect`. */\n public selectEllipse(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(ellipseSelection(width, height, rect, options), options);\n }\n\n /** Lasso / polygonal lasso. */\n public selectPolygon(points: ImagePoint[], options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(polygonSelection(width, height, points, options), options);\n }\n\n /**\n * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker\n * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)\n * these are also the Background layer's pixels.\n */\n public setSamplePixels(pixels: PixelBuffer | null): void {\n this.samplePixels = pixels;\n this.samplePixelsOverride = pixels !== null;\n }\n\n /**\n * The pixels the selection tools sample: the source image at natural size, or with\n * `sampleMerged` the composite of the layers (no crop, rotation or annotations). `null`\n * without pixel access (no DOM, tainted canvas).\n */\n public async getSamplePixels(options: SampleOptions = {}): Promise<PixelBuffer | null> {\n const { width, height } = this.naturalSize;\n let base = options.pixels ?? this.samplePixels;\n if (!base) {\n try {\n const canvas = await this.renderSource();\n const ctx = canvas.getContext('2d');\n const data = ctx?.getImageData?.(0, 0, width, height);\n if (data && data.data && data.data.length >= width * height * 4) {\n base = { width, height, data: data.data };\n this.samplePixels = base;\n this.samplePixelsOverride = false;\n }\n } catch {\n base = null;\n }\n }\n if (!base) return null;\n if (base.width !== width || base.height !== height) {\n if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;\n return null;\n }\n const legacy = legacyView(this.stack(), width, height, null).exact;\n if (options.sample === 'active-layer') {\n const layer = this.getActiveLayer();\n if (!layer || layer.type !== 'pixel') {\n throw new LayerOperationError('Sampling the active layer needs an active pixel layer; select one, or sample the composite.');\n }\n const surface = layer.source === 'document' && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);\n return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };\n }\n if (legacy && !options.sampleMerged) return base;\n this.syncViews();\n if (legacy && !options.pixels) {\n // The old shape: the source through the base adjustments and adjustment layers.\n const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data as ArrayLike<number>) };\n applyAdjustments(copy as unknown as ImageData, legacyView(this.stack(), width, height, null).adjustments);\n return copy;\n }\n const pixels = options.pixels;\n const sources: CompositorSources = {\n width,\n height,\n surfaceOf: (l) => (l.type === 'pixel' && l.source === 'document' && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l)),\n version: pixels ? 'pixels' : this.sourceVersion(),\n };\n // Without `sampleMerged`, the pixels as painted: adjustment layers and filters left out.\n const stack = withoutAnnotations(this.state.layers);\n const layers = options.sampleMerged ? stack : pixelsOnly(stack);\n const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);\n return { width, height, data };\n }\n\n private async requirePixels(options: SampleOptions): Promise<PixelBuffer> {\n const pixels = await this.getSamplePixels(options);\n if (!pixels) throw new Error('The image pixels are not available (no canvas access); pass `pixels`.');\n return pixels;\n }\n\n /**\n * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when\n * nothing is selected).\n */\n public async magicWand(\n point: ImagePoint,\n options: MagicWandOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(magicWand(pixels, point, options), options);\n return this.getSelection();\n }\n\n /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */\n public async selectColorRange(\n samples: ColorRangeSample[],\n options: ColorRangeOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(colorRange(pixels, samples, options), options);\n return this.getSelection();\n }\n\n /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */\n public modifySelection(op: SelectionModifyOp, radius: number): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n return this.setSelection(modifySelectionMask(sel, op, radius));\n }\n\n /**\n * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)\n * to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.\n */\n public async refineSelection(params: RefineEdgeParams, options: SampleOptions = {}): Promise<boolean> {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;\n return this.setSelection(refineSelectionMask(pixels, sel, params));\n }\n\n /**\n * Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or\n * RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.\n */\n public async loadSelectionFromImage(\n source: string | PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number; mode?: SelectionMode } = {}\n ): Promise<SelectionMask | null> {\n let pixels: PixelBuffer;\n if (typeof source === 'string') {\n const img = await this.renderer.imageCache.get(source);\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const canvas = this.renderer.createCanvas(w, h);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, 0, 0, w, h);\n const data = ctx?.getImageData?.(0, 0, w, h);\n if (!data) throw new Error('The mask image pixels are not available (no canvas access); pass pixels.');\n pixels = { width: w, height: h, data: data.data };\n } else {\n pixels = source;\n }\n this.setSelection(selectionFromPixels(pixels, options), options.mode ?? 'replace');\n return this.getSelection();\n }\n\n /** Quick Mask view (tints the unselected area). View state: no undo step. */\n public setQuickMask(on: boolean): void {\n if (Boolean(this.state.quickMask) === on) return;\n this.state.quickMask = on;\n this.notify();\n }\n\n // --- Layer masks ---\n\n private maskFromSelection(sel: SelectionMask, props: LayerMaskProps = {}): RasterMask {\n const { width, height } = this.naturalSize;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const mask: RasterMask = { surface: surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0), defaultValue: 0 };\n return withMaskProps(mask, props);\n }\n\n /**\n * Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo\n * step. Without a selection, returns false.\n */\n public applySelectionAsLayerMask(layerId: string, props: LayerMaskProps = {}): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, this.maskFromSelection(sel, props));\n }\n\n private setLayerMask(layerId: string, mask: RasterMask | null, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, layerId);\n if (!loc) return false;\n if (commitToHistory) this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n if (!mask && this.state.activeLayerId === layerId) this.state.activeTarget = 'pixels';\n this.notify();\n return true;\n }\n\n /** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */\n public setAdjustmentLayerMask(layerId: string, mask: SelectionMask | null, props: LayerMaskProps = {}): boolean {\n if (!this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, mask ? this.maskFromSelection(mask, props) : null);\n }\n\n /** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */\n public setAdjustmentLayerMaskProps(layerId: string, props: LayerMaskProps, commitToHistory = true): boolean {\n const layer = this.getLayer(layerId);\n if (!layer || layer.type !== 'adjustment' || !layer.mask) return false;\n return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);\n }\n\n /** Load Selection from a layer mask (its pixels; density and feather are not applied). */\n public selectionFromLayerMask(layerId: string, mode: SelectionMode = 'replace'): boolean {\n const layer = this.getLayer(layerId);\n if (!layer?.mask || layer.mask.source) return false;\n const { width, height } = this.naturalSize;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });\n const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));\n return this.setSelection(sel, mode);\n }\n\n // --- Histogram ---\n\n /**\n * Histogram of the last rendered preview (after adjustments and layers, before annotations),\n * or `null` before the first render or without canvas pixel access.\n */\n public getHistogram(): ImageHistogram | null {\n return this.renderer.previewHistogram;\n }\n\n /** Calls `listener` now and whenever a render produces a new preview histogram. */\n public subscribeHistogram(listener: (histogram: ImageHistogram | null) => void): () => void {\n this.histogramListeners.add(listener);\n listener(this.renderer.previewHistogram);\n return () => {\n this.histogramListeners.delete(listener);\n };\n }\n\n // --- Depth Masking (Gleamforge) ---\n\n /** Sets the depth mask (a parametric mask on the active pixel layer, or the Background) as one undo step. */\n public setDepthMask(partial: Partial<DepthMaskOptions>): void {\n this.recordHistory();\n this.syncViews();\n const target = this.developLayer();\n if (target) {\n const merged: DepthMaskOptions = { ...this.state.depthMask, ...partial };\n merged.depthRange = [...merged.depthRange] as [number, number];\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n if (isDefaultDepthMask(merged)) {\n if (next.mask?.source) delete next.mask;\n } else next.mask = depthRasterMask(merged);\n return next;\n });\n }\n this.notify();\n }\n\n public resetDepthMask(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target?.mask?.source) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n delete next.mask;\n return next;\n });\n }\n this.notify();\n }\n\n // --- Annotations & Layers ---\n\n private annotationLayer(id: string): AnnotationLayer | undefined {\n return flattenLayers(this.state.layers).find(\n (l): l is AnnotationLayer => l.type === 'annotation' && l.annotation.id === id\n );\n }\n\n /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */\n public addAnnotation(annotation: CreateAnnotationPayload, placement: AnnotationPlacement = {}): string {\n this.recordHistory();\n const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n const fullAnnotation = { ...annotation, id } as Annotation;\n const layer = annotationLayerFor(fullAnnotation);\n const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;\n this.state.layers = loc\n ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1)\n : insertLayer(this.state.layers, layer, null);\n this.state.selectedAnnotationId = id;\n this.notify();\n return id;\n }\n\n public updateAnnotation(id: string, partial: Partial<Annotation>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const layer = this.annotationLayer(id);\n if (layer) {\n const annotation = { ...layer.annotation, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));\n }\n this.notify();\n }\n\n public removeAnnotation(id: string): void {\n this.recordHistory();\n const layer = this.annotationLayer(id);\n if (layer) this.state.layers = mapLayer(this.state.layers, layer.id, () => null);\n if (this.state.selectedAnnotationId === id) {\n this.state.selectedAnnotationId = null;\n }\n this.notify();\n }\n\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.layers = layers;\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n public selectAnnotation(id: string | null): void {\n this.state.selectedAnnotationId = id;\n this.notify();\n }\n\n // --- Zoom & Pan ---\n\n public setZoom(zoom: number): void {\n this.state.zoom = Math.max(0.1, Math.min(10, zoom));\n this.notify();\n }\n\n public zoomIn(factor = 1.2): void {\n this.setZoom(this.state.zoom * factor);\n }\n\n public zoomOut(factor = 1.2): void {\n this.setZoom(this.state.zoom / factor);\n }\n\n public resetZoom(): void {\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.notify();\n }\n\n public setPan(pan: ImagePoint): void {\n this.state.pan = pan;\n this.notify();\n }\n\n public setComparing(comparing: boolean): void {\n if (this.state.isOriginalCompared === comparing) return;\n this.state.isOriginalCompared = comparing;\n this.notify();\n }\n\n public setFit(fit: ImageEditorFit): void {\n if (this.state.fit === fit) return;\n this.state.fit = fit;\n this.notify();\n }\n\n // --- Rendering & Export ---\n\n private blankSource: HTMLCanvasElement | null = null;\n\n /**\n * What the renderer draws from: the source image, or, for a document without one (an opened\n * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every\n * pixel then comes from the layers. `null` when there is nothing to draw.\n */\n private drawSource(): CanvasImageSource | string | null {\n const source = this.imageElement || this.state.sourceUrl;\n if (source) return source;\n if (!this.isSourceless()) return null;\n const { width, height } = this.naturalSize;\n if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {\n this.blankSource = this.renderer.createCanvas(width, height);\n }\n return this.blankSource;\n }\n\n public async render(targetCanvas: HTMLCanvasElement, options?: RenderOptions): Promise<void> {\n const source = this.drawSource();\n if (!source) return;\n this.syncViews();\n const dirty = this.pendingDirty;\n this.pendingDirty = [];\n try {\n await this.renderer.renderToCanvas(targetCanvas, source, this.state, {\n ...options,\n dirty: dirty === 'all' ? undefined : dirty,\n checkpointLayerId: this.state.activeLayerId,\n });\n } catch {\n // In non-DOM or test mock environments without image loading, safely ignore\n }\n }\n\n public async renderExport(options?: ExportOptions): Promise<HTMLCanvasElement> {\n const source = this.drawSource();\n if (!source) {\n return this.renderer.createCanvas(1, 1);\n }\n this.syncViews();\n if (options?.layerIds) {\n const keep = new Set(options.layerIds);\n const layers = flattenLayers(this.state.layers).filter((l) => keep.has(l.id));\n return this.renderer.renderExport(source, { ...this.state, layers }, options);\n }\n return this.renderer.renderExport(source, this.state, options);\n }\n\n /**\n * Renders the edited image (adjustments, layers and, by default, annotations) in the\n * source image's own orientation and frame, ignoring crop, rotation and flips, so it lines\n * up with natural-pixel coordinates (selections, `select_region`). `scale` ≤ 1 shrinks it.\n * Returns `null` when no image is loaded.\n */\n public async renderSourceView(options: { scale?: number; includeAnnotations?: boolean } = {}): Promise<HTMLCanvasElement | null> {\n const source = this.drawSource();\n if (!source) return null;\n this.syncViews();\n const { width, height } = this.state.imageDimensions;\n const k = Math.min(1, Math.max(0, options.scale ?? 1));\n return this.renderer.renderExport(\n source,\n { ...this.state, crop: null, transform: { rotate: 0, flipH: false, flipV: false } },\n {\n width: Math.max(1, Math.round(width * k)),\n height: Math.max(1, Math.round(height * k)),\n includeAnnotations: options.includeAnnotations,\n }\n );\n }\n\n public async toDataURL(options: ExportOptions = {}): Promise<string> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n return canvas.toDataURL(format, quality);\n }\n\n public async toBlob(options: ExportOptions = {}): Promise<Blob> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Failed to create Blob from canvas'));\n }, format, quality);\n } else {\n // Fallback for environments lacking canvas.toBlob\n try {\n const dataUrl = canvas.toDataURL(format, quality);\n const parts = dataUrl.split(',');\n const byteStr = atob(parts[1]);\n const ab = new ArrayBuffer(byteStr.length);\n const ia = new Uint8Array(ab);\n for (let i = 0; i < byteStr.length; i++) {\n ia[i] = byteStr.charCodeAt(i);\n }\n resolve(new Blob([ab], { type: format }));\n } catch (err) {\n reject(err);\n }\n }\n });\n }\n\n // --- AI Masking & Transformation Helpers ---\n\n public async renderMask(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<HTMLCanvasElement> {\n this.syncViews();\n return this.renderer.renderMask(this.state, options);\n }\n\n public async toMaskDataURL(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<string> {\n const canvas = await this.renderMask(options);\n return canvas.toDataURL('image/png');\n }\n\n /**\n * Renders the current source image at natural size with no crop, rotation, flip,\n * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with\n * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.\n */\n public async renderSource(): Promise<HTMLCanvasElement> {\n const { width, height } = this.state.imageDimensions;\n const canvas = this.renderer.createCanvas(width, height);\n const source = this.imageElement || this.state.sourceUrl;\n const ctx = canvas.getContext('2d');\n if (!ctx || !source) return canvas;\n const img = typeof source === 'string' ? await this.renderer.imageCache.get(source) : source;\n ctx.drawImage(img, 0, 0, width, height);\n return canvas;\n }\n\n /**\n * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the\n * Background, or the composite of the layers without annotations, in the source frame, when\n * they go to a new layer or mask (see `applyInpaintedImage`).\n */\n public async toAIInputDataURL(options: AIResultOptions = {}): Promise<string> {\n const target = options.target ?? this.defaultAITarget();\n if (target === 'replace') return this.toSourceDataURL();\n const canvas = await this.renderSourceView({ includeAnnotations: false });\n return canvas ? canvas.toDataURL('image/png') : this.toSourceDataURL();\n }\n\n public async toSourceDataURL(options: Pick<ExportOptions, 'format' | 'quality'> = {}): Promise<string> {\n const canvas = await this.renderSource();\n return canvas.toDataURL(options.format || 'image/png', options.quality ?? 0.92);\n }\n\n /**\n * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment\n * and annotation layers are edits of the image, not images of their own).\n */\n /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */\n private defaultAITarget(): 'new-layer' | 'replace' {\n return this.isMultiLayer || this.isSourceless() ? 'new-layer' : 'replace';\n }\n\n /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */\n private isSourceless(): boolean {\n if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;\n return !flattenLayers(this.state.layers).some((l) => l.type === 'pixel' && l.source === 'document');\n }\n\n public get isMultiLayer(): boolean {\n return flattenLayers(this.state.layers).filter((l) => l.type === 'pixel' || l.type === 'group').length > 1;\n }\n\n /**\n * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.\n * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the\n * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise\n * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears\n * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.\n */\n public async applyInpaintedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, true);\n return;\n }\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const data = new Uint8ClampedArray(px.data as ArrayLike<number>);\n const sel = this.state.selection?.bounds ? this.state.selection : null;\n if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round((data[i] * sel.data[p]) / 255);\n return this.addPixelLayer({ name: 'Inpaint', pixels: { width, height, data } });\n }\n\n /**\n * Applies a background-removed result as ONE undoable step. In a document with several layers\n * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer\n * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer\n * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the\n * new image has different dimensions). Resolves to the masked layer's id in mask mode.\n */\n public async applyBackgroundRemovedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, false);\n return;\n }\n const target = this.developLayer();\n if (!target) throw new LayerOperationError('There is no pixel layer to mask.');\n requireUnlocked(target, 'all', 'mask it');\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const alpha = new Uint8ClampedArray(width * height);\n for (let p = 0; p < alpha.length; p++) alpha[p] = (px.data as ArrayLike<number>)[p * 4 + 3];\n let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);\n if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);\n this.commitLayers(setLayerMaskOp(this.state.layers, target.id, { surface, defaultValue: 0 }));\n return target.id;\n }\n\n /**\n * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and\n * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo\n * step. Returns the layer id.\n */\n public async rasterizeLayer(id: string): Promise<string> {\n const layer = requireLayer(this.state.layers, id).layer;\n requireUnlocked(layer, 'all', 'rasterize it');\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n let next: PixelLayer;\n if (layer.type === 'annotation') {\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const surface = await this.compositeToSurface([content], this.paintBounds(layer));\n next = { type: 'pixel', id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };\n if (layer.clipped) next.clipped = true;\n if (layer.mask) next.mask = layer.mask;\n if (layer.locks) next.locks = layer.locks;\n if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;\n } else if (layer.type === 'pixel') {\n if (!layer.filters?.length) throw new LayerOperationError(`Layer \"${layer.name || id}\" is already pixels with no filters to bake.`);\n requireUnlocked(layer, 'pixels', 'rasterize it');\n const spatial = layer.filters.some((f) => f.enabled && f.kind === 'blur');\n const content: PixelLayer = { ...layer, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;\n const surface = await this.compositeToSurface([content], rect);\n next = { ...layer, surface, offset: { x: 0, y: 0 } };\n delete next.filters;\n delete next.source;\n if (layer.mask && (layer.offset.x || layer.offset.y)) {\n // The mask lives in layer space: keep it where it was in the document.\n next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };\n }\n } else if (layer.type === 'smart') {\n return this.rasterizeSmartObject(id);\n } else {\n throw new LayerOperationError(`Only annotation layers, smart objects and pixel layers with filters rasterize; \"${layer.name || id}\" is a ${layer.type} layer. Merge it down or flatten instead.`);\n }\n this.commitLayers(mapLayer(this.state.layers, id, () => next));\n return id;\n }\n\n // --- PSD ---\n\n /** Pixels per inch of the last opened PSD (written back by `toPsd`). */\n private psdResolution: number | undefined;\n\n /**\n * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the\n * document has no source image afterwards (every pixel comes from the layers). What the model\n * cannot hold is approximated or left out; `warnings` says what, naming the layers.\n * Throws `PsdError` for files that cannot be read at all.\n */\n public loadPsd(bytes: Uint8Array): { layerIds: string[]; warnings: string[]; width: number; height: number; summary: PsdImportResult['summary'] } {\n const result = importPsd(bytes, { newId: () => newLayerId() });\n this.abandonContents();\n this.recordHistory();\n const s = this.state;\n this.imageElement = null;\n s.sourceUrl = null;\n s.imageDimensions = { width: result.width, height: result.height };\n if (!this.samplePixelsOverride) this.samplePixels = null;\n s.layers = result.layers;\n s.crop = null;\n s.transform = { ...DEFAULT_TRANSFORM_VALUES };\n s.outputOpacity = 1;\n delete s.legacyOutputBlur;\n s.selection = null;\n s.selectedAnnotationId = null;\n const pixels = flattenLayers(result.layers).filter((l) => l.type === 'pixel');\n s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;\n s.activeTarget = 'pixels';\n this.psdResolution = result.resolution;\n if (result.patterns.length) s.patterns = result.patterns;\n else delete s.patterns;\n if (result.globalLight) s.globalLight = result.globalLight;\n else delete s.globalLight;\n this.lastLoadProblems = result.warnings;\n this.notify();\n return {\n layerIds: flattenLayers(result.layers).map((l) => l.id),\n warnings: result.warnings,\n width: result.width,\n height: result.height,\n summary: result.summary,\n };\n }\n\n /**\n * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked\n * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left\n * out), and the merged image is our composite. `warnings` lists every such change.\n */\n public async toPsd(): Promise<{ bytes: Uint8Array; warnings: string[]; psb: boolean; width: number; height: number }> {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n const warnings: string[] = [];\n const note = new Map<string, string[]>();\n const add = (msg: string, name: string) => note.set(msg, [...(note.get(msg) ?? []), name]);\n const canRasterize = this.renderer.canRasterize();\n await this.renderer.prepareLayerAssets(this.state.layers, width, height);\n const sources = this.renderer.compositorSources(width, height);\n const compositor = new LayerCompositor();\n\n const bakeMask = (layer: ImageLayer): RasterMask | undefined => {\n const mask = layer.mask;\n if (!mask?.source) return mask;\n add('Parametric (depth) masks were baked into pixel masks', layer.name);\n const plain = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } } as ImageLayer;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const coverage = compositor.maskCoverage(plain, canvas, sources);\n const bytes = new Uint8ClampedArray(width * height);\n if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);\n else bytes.fill(255);\n const baked: RasterMask = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };\n if (mask.density !== undefined && mask.density < 1) baked.density = mask.density;\n if (mask.feather) baked.feather = mask.feather;\n if (mask.enabled === false) baked.enabled = false;\n return baked;\n };\n\n const prepare = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n const p = await prepareLayer(layer);\n // `exportPsd` writes it as Normal; the merged image keeps the paint mix.\n if (p?.blend === 'pigment') add('The Pigment blend mode has no PSD equivalent and was written as Normal', layer.name);\n return p;\n };\n\n const prepareLayer = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n if (layer.type === 'group') {\n const children: ImageLayer[] = [];\n for (const c of layer.children) {\n const p = await prepare(c);\n if (p) children.push(p);\n }\n return { ...layer, children, mask: bakeMask(layer) } as ImageLayer;\n }\n if (layer.type === 'adjustment') return { ...layer, mask: bakeMask(layer) } as ImageLayer;\n if (layer.type === 'annotation') {\n if (!canRasterize) {\n add('Annotation layers need a canvas (a browser) to rasterize and were left out', layer.name);\n return null;\n }\n add('Annotation layers were written as pixel layers', layer.name);\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend,\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.locks) out.locks = layer.locks;\n if (layer.effects) out.effects = layer.effects;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n return out;\n }\n if (layer.type === 'smart') {\n 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 for (const l of flattenLayers(layers)) {\n if (l.type === 'annotation') {\n layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));\n }\n }\n }\n // The Background now shows the new source.\n const bg = developTarget(layers, null);\n if (bg && bg.source !== 'document') layers = mapLayer(layers, bg.id, () => ({ ...bg, source: 'document' }));\n\n if (clearAnnotations) {\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.selectedAnnotationId = null;\n }\n this.state.layers = layers;\n this.notify();\n }\n\n // --- Serialization ---\n\n /**\n * The serializable state. A document whose layers still have the v1 / v2 shape (the\n * Background with its sliders and depth mask, adjustment layers, annotations) is written as\n * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG\n * assets. `selection` is included (run-length encoded) only when something is selected.\n */\n public toJSON(): SerializedImageEditorState;\n /**\n * With `assets: 'external'`, layer pixels go to `store` (each PNG uploaded once per store) and\n * the v3 document holds the store's references. Old-shape documents have no assets either way.\n */\n public toJSON(options: { assets: 'external'; store: ImageEditorAssetStore }): Promise<SerializedImageEditorState>;\n public toJSON(options?: { assets: 'external'; store: ImageEditorAssetStore }): SerializedImageEditorState | Promise<SerializedImageEditorState> {\n // While contents are open, the document is the one set aside (without the open edits).\n const root = this.contentsStack[0]?.saved;\n const current = this.state;\n const element = this.imageElement;\n if (root) {\n this.state = root.state;\n this.imageElement = root.element;\n this.viewKey = [];\n }\n let out: SerializedImageEditorState;\n try {\n out = this.serializeState();\n if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);\n } finally {\n if (root) {\n this.state = current;\n this.imageElement = element;\n this.viewKey = [];\n }\n }\n if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;\n return serializeLayersExternal(this.rootLayers(), options.store).then(({ layers, assets, smartContents }) => (smartContents ? { ...out, layers, assets, smartContents } : { ...out, layers, assets }));\n }\n\n /**\n * Loads a stored state whose v3 assets may live in an asset store (references other than data\n * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left\n * out and listed in `lastLoadProblems`.\n */\n public async loadJSONAsync(data: SerializedImageEditorState, options: { store?: ImageEditorAssetStore } = {}): Promise<void> {\n if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {\n this.loadJSON(data);\n return;\n }\n const problems: string[] = [];\n const assets = await resolveExternalAssets(data.assets, options.store, problems);\n this.loadJSON({ ...data, assets });\n this.lastLoadProblems = [...problems, ...this.lastLoadProblems];\n }\n\n /** Loads a stored state of any version (one undo step). */\n public loadJSON(data: SerializedImageEditorState): void {\n this.abandonContents();\n this.recordHistory();\n this.applyStored(data);\n let selection: SelectionMask | null = null;\n if (data.selection) {\n try {\n selection = normalizeSelection(deserializeSelection(data.selection));\n } catch {\n selection = null;\n }\n }\n this.state.selection = selection;\n this.dropMismatchedSelection();\n this.notify();\n }\n\n /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */\n public lastLoadProblems: string[] = [];\n\n private applyStored(raw: Partial<SerializedImageEditorState>): void {\n const s = this.state;\n const data = migrateSerializedImageEditorState(raw as SerializedImageEditorState);\n this.lastLoadProblems = [];\n if (isLayered(data)) {\n const problems: string[] = [];\n const canvas = typeof data.width === 'number' && data.width > 0 && typeof data.height === 'number' && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;\n s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });\n this.lastLoadProblems = problems;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.outputOpacity = data.opacity;\n if (typeof data.legacyOutputBlur === 'number' && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;\n const light = data.globalLight;\n if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };\n else delete s.globalLight;\n const patterns = deserializePatterns(data.patterns, problems);\n if (patterns.length) s.patterns = patterns;\n else delete s.patterns;\n if (!this.imageElement && typeof data.width === 'number' && typeof data.height === 'number' && data.width > 0 && data.height > 0) {\n s.imageDimensions = { width: data.width, height: data.height };\n }\n } else {\n const { width, height } = this.naturalSize;\n const stack = layersFromLegacy(data, width, height);\n s.layers = stack.layers;\n s.outputOpacity = stack.outputOpacity;\n if (stack.legacyOutputBlur !== undefined) s.legacyOutputBlur = stack.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.activeLayerId = null;\n delete s.globalLight;\n delete s.patterns;\n }\n s.activeTarget = 'pixels';\n this.viewKey = [];\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) s.selectedAnnotationId = null;\n }\n\n private serializeState(): SerializedImageEditorState {\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, null);\n // A global light is only stored in the layered schema.\n if (view.exact && !this.state.globalLight && !this.state.patterns?.length) {\n return {\n // States without adjustment layers keep schema 1, so they round-trip unchanged.\n version: view.adjustments.layers?.length ? 2 : 1,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n adjustments: serializeAdjustments(view.adjustments),\n depthMask: {\n ...view.depthMask,\n depthRange: [...view.depthMask.depthRange] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(view.annotations)),\n };\n }\n const { layers, assets, smartContents } = serializeLayers(this.state.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width,\n height,\n layers,\n assets,\n activeLayerId: this.state.activeLayerId,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n opacity: this.state.outputOpacity,\n };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };\n if (this.state.patterns?.length) out.patterns = serializePatterns(this.state.patterns);\n if (smartContents) out.smartContents = smartContents;\n return out;\n }\n}\n\n/**\n * Legacy-process adjustments are saved without `process` (and without an unused zero `tint`), so\n * they serialize exactly as they did before process versions existed.\n */\nfunction serializeAdjustments(adjustments: ImageAdjustments): ImageAdjustments {\n const out: ImageAdjustments = { ...adjustments };\n if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));\n else delete out.layers;\n if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {\n delete out.process;\n if (!out.tint) delete out.tint;\n }\n return out;\n}\n\n/** A pixel layer with its develop filter updated from base-slider values (created if missing). */\nfunction withDevelop(layer: PixelLayer, partial: Partial<ImageAdjustments>): PixelLayer {\n const filters = [...(layer.filters ?? [])];\n let i = filters.findIndex((f) => f.kind === 'develop');\n if (i === -1) {\n filters.unshift({\n id: layer.source === 'document' ? DEVELOP_FILTER_ID : newLayerId('flt'),\n kind: 'develop',\n enabled: true,\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n process: LATEST_PROCESS_VERSION,\n });\n i = 0;\n }\n const next = { ...(filters[i] as DevelopFilter) };\n for (const k of DEVELOP_KEYS) {\n const v = partial[k];\n if (typeof v === 'number' && Number.isFinite(v)) next[k] = v;\n }\n if ('process' in partial) next.process = normalizeProcessVersion(partial.process);\n filters[i] = next;\n return { ...layer, filters };\n}\n\n/** A pixel layer with its blur filter set to `radius` (removed at 0). */\nfunction withBlur(layer: PixelLayer, radius: number): PixelLayer {\n const existing = layer.filters?.find((f) => f.kind === 'blur');\n const filters: LayerFilter[] = (layer.filters ?? []).filter((f) => f.kind !== 'blur');\n if (radius > 0) {\n filters.push({\n id: existing?.id ?? (layer.source === 'document' ? BLUR_FILTER_ID : newLayerId('flt')),\n kind: 'blur',\n enabled: true,\n radius,\n });\n }\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return next;\n}\n\n/** Updates a node from an old-shape layer (`setAdjustments({ layers })`), keeping a raster mask whose pixels did not change. */\nfunction mergeAdjustmentNode(node: AdjustmentLayerNode, update: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const next = {\n ...node,\n kind: update.kind,\n params: JSON.parse(JSON.stringify(update.params)),\n visible: update.enabled !== false,\n opacity: update.opacity,\n blend: update.blend,\n name: update.name ?? '',\n } as AdjustmentLayerNode;\n if (!update.mask) delete next.mask;\n else {\n const current = node.mask && !node.mask.source ? node.mask : null;\n const memo = current ? legacyMaskOf(current, width, height) : null;\n if (current && memo && memo.width === update.mask.width && memo.height === update.mask.height && memo.rle === update.mask.rle) {\n next.mask = withMaskProps(current, {\n density: update.mask.density ?? 1,\n feather: update.mask.feather ?? 0,\n enabled: update.mask.enabled !== false,\n });\n } else {\n const m = rasterMaskFromLegacy(update.mask, width, height);\n if (m) next.mask = m;\n else delete next.mask;\n }\n }\n return next;\n}\n\n/** Applies mask property changes; `undefined` fields keep their value, defaults are left out. */\nfunction withMaskProps(mask: RasterMask, props: LayerMaskProps): RasterMask {\n const next: RasterMask = { ...mask };\n if (props.density !== undefined) {\n const d = Math.min(1, Math.max(0, props.density));\n if (d !== 1) next.density = d;\n else delete next.density;\n }\n if (props.feather !== undefined) {\n const f = Math.min(1000, Math.max(0, props.feather));\n if (f) next.feather = f;\n else delete next.feather;\n }\n if (props.enabled !== undefined) {\n if (props.enabled) delete next.enabled;\n else next.enabled = false;\n }\n if (props.linked !== undefined) {\n if (props.linked) delete next.linked;\n else next.linked = false;\n }\n return next;\n}\n\n/** Rescales adjustment-layer masks (canvas-sized) when the canvas size changes. */\nfunction resizeLegacyMasks(layers: ImageLayer[], from: ImageDimensions, to: ImageDimensions): ImageLayer[] {\n const fw = Math.max(1, Math.round(from.width));\n const fh = Math.max(1, Math.round(from.height));\n const tw = Math.max(1, Math.round(to.width));\n const th = Math.max(1, Math.round(to.height));\n if (fw === tw && fh === th) return layers;\n let out = layers;\n for (const l of flattenLayers(layers)) {\n if (l.type !== 'adjustment' || !l.mask || l.mask.source) continue;\n const old = l.mask;\n const mask = rasterMaskFromLegacy(legacyMaskOf(old, fw, fh), tw, th);\n if (!mask) continue;\n out = mapLayer(out, l.id, (n) => ({ ...n, mask: { ...mask, density: old.density, feather: old.feather, enabled: old.enabled } }));\n }\n return out;\n}\n\n/** Whether every pixel of `surface` inside the canvas is opaque. */\nfunction isOpaqueInside(surface: PixelSurface, width: number, height: number): boolean {\n const row = new Uint8ClampedArray(width * 4);\n for (let y = 0; y < height; y++) {\n readRegion(surface, { x: 0, y, width, height: 1 }, row);\n for (let i = 3; i < row.length; i += 4) if (row[i] !== 255) return false;\n }\n return true;\n}\n\nfunction collectStateTiles(layers: readonly ImageLayer[], into: Set<Tile>): void {\n for (const l of flattenLayers(layers)) {\n if (l.type === 'pixel' && l.source !== 'document') collectTiles(l.surface, into);\n if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);\n if (l.type === 'smart') {\n collectTiles(l.surface, into);\n if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);\n if (l.contents.raster) collectTiles(l.contents.raster, into);\n if (l.contents.document) collectStateTiles(l.contents.document.layers, into);\n }\n }\n}\n\n/** The stack without adjustment layers and layer filters (the pixels as painted). */\nfunction pixelsOnly(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'adjustment') continue;\n if (l.type === 'group') out.push({ ...l, children: pixelsOnly(l.children) });\n else if (l.type === 'pixel' && l.filters?.length) {\n const p = { ...l };\n delete p.filters;\n out.push(p);\n } else out.push(l);\n }\n return out;\n}\n\n/** Bilinear resample of RGBA pixels to `width × height`. */\nexport function resizePixels(px: PixelBuffer, width: number, height: number): PixelBuffer {\n const src = px.data as ArrayLike<number>;\n const out = new Uint8ClampedArray(width * height * 4);\n const kx = px.width / width;\n const ky = px.height / height;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(px.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(px.width - 1, x0 + 1);\n const tx = fx - x0;\n for (let c = 0; c < 4; c++) {\n const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;\n const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;\n out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;\n }\n }\n }\n return { width, height, data: out };\n}\n\nfunction withoutAnnotations(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: withoutAnnotations(l.children) } : l);\n }\n return out;\n}\n\n/** A colour as RGBA bytes: `#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` (0..255). */\nexport function parseColor(value: string | number[]): [number, number, number, number] {\n if (Array.isArray(value)) {\n if (value.length < 3 || value.length > 4 || !value.every((v) => typeof v === 'number' && v >= 0 && v <= 255)) {\n throw new LayerOperationError('A colour array is [r, g, b] or [r, g, b, a] with values 0..255.');\n }\n return [Math.round(value[0]), Math.round(value[1]), Math.round(value[2]), Math.round(value[3] ?? 255)];\n }\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(String(value).trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${value}\"; use #rrggbb or #rrggbbaa.`);\n let hex = m[1];\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n const n = (i: number) => parseInt(hex.slice(i, i + 2), 16);\n return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];\n}\n\n/** `#rrggbb` of RGBA bytes (alpha dropped). */\nexport function toHex(c: readonly number[]): string {\n return '#' + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');\n}\n\n/** `null` for an empty selection, so \"nothing selected\" has one representation. */\nfunction normalizeSelection(mask: SelectionMask | null): SelectionMask | null {\n return mask?.bounds ? mask : null;\n}\n\nfunction sameSelection(a: SelectionMask, b: SelectionMask): boolean {\n if (a.width !== b.width || a.height !== b.height) return false;\n const x = a.data;\n const y = b.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n return true;\n}\n\n/** Scales an annotation's geometry from one image size to another. */\nfunction scaleAnnotation(ann: Annotation, sx: number, sy: number): Annotation {\n const k = Math.sqrt(Math.abs(sx * sy));\n const base = { ...ann, x: ann.x * sx, y: ann.y * sy } as Annotation;\n switch (base.type) {\n case 'pen':\n return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };\n case 'rect':\n return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };\n case 'image':\n return { ...base, width: base.width * sx, height: base.height * sy };\n case 'circle':\n return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };\n case 'arrow':\n case 'line':\n return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };\n case 'text':\n return { ...base, fontSize: base.fontSize * k };\n }\n}\n","import {\n loadDocument,\n type JsonObject,\n type LoadReport,\n type MigrationStep,\n} from '../persistence/migrate';\nimport { DEFAULT_DEPTH_MASK, DEFAULT_TRANSFORM, normalizeStoredAdjustments } from './controller';\nimport type {\n Annotation,\n CropBox,\n DepthMaskOptions,\n ImageAdjustments,\n ImageTransform,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { deserializeSelection } from './selection/mask';\nimport type { SerializedLayer, SerializedLayeredImageEditorState } from './layers/serialize';\n\n/**\n * The newest `version` that `ImageEditorController.toJSON()` writes (3: layers). Documents whose\n * layers still have the old shape are written as version 1 or 2.\n */\nexport const IMAGE_EDITOR_STATE_VERSION = 3;\n\n/**\n * Migration steps for the document-level schema (versions 1 and 2), keyed by the version they\n * start from. Empty: version 2 only adds `adjustments.layers`, which version 1 readers keep.\n */\nexport const imageEditorStateMigrations: Record<number, MigrationStep> = {};\n\nexport interface ImageEditorDocumentLoad {\n /** State to pass to `loadJSON` / `initialState`. */\n state: SerializedImageEditorState;\n report: LoadReport;\n /** Top-level keys this build does not know; spread them into the saved JSON to keep them. */\n unknown: JsonObject;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\nfunction expectObject(value: unknown, what: string): Record<string, unknown> {\n if (!isObject(value)) throw new Error(`${what} must be an object`);\n return value;\n}\n\nfunction checkFields(\n value: Record<string, unknown>,\n types: Record<string, (v: unknown) => boolean>,\n what: string,\n) {\n for (const [field, ok] of Object.entries(types)) {\n if (field in value && value[field] !== undefined && !ok(value[field])) {\n throw new Error(`${what}.${field} has an invalid value`);\n }\n }\n}\n\nconst isBoolean = (v: unknown) => typeof v === 'boolean';\n\nfunction parseCrop(value: unknown): CropBox | null {\n if (value === null) return null;\n const crop = expectObject(value, 'crop');\n for (const field of ['x', 'y', 'width', 'height'] as const) {\n if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);\n }\n return { ...(crop as unknown as CropBox) };\n}\n\nfunction parseTransform(value: unknown): ImageTransform {\n const transform = expectObject(value, 'transform');\n checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, 'transform');\n return { ...DEFAULT_TRANSFORM, ...(transform as Partial<ImageTransform>) };\n}\n\nconst ADJUSTMENT_NUMBERS = [\n 'brightness',\n 'contrast',\n 'saturation',\n 'exposure',\n 'temperature',\n 'tint',\n 'blur',\n 'opacity',\n] as const;\n\nfunction parseAdjustments(value: unknown): ImageAdjustments {\n const adjustments = expectObject(value, 'adjustments');\n checkFields(\n adjustments,\n {\n ...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),\n process: (v) => v === 1 || v === 2,\n },\n 'adjustments',\n );\n // Kept as stored: `loadJSON` fills defaults and keeps a missing `process` legacy.\n return { ...(adjustments as unknown as ImageAdjustments) };\n}\n\nfunction parseDepthMask(value: unknown): DepthMaskOptions {\n const mask = expectObject(value, 'depthMask');\n checkFields(\n mask,\n {\n enabled: isBoolean,\n maskSource: (v) => v === null || typeof v === 'string',\n mode: (v) => v === 'threshold' || v === 'brightness',\n depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),\n softness: isNumber,\n invert: isBoolean,\n },\n 'depthMask',\n );\n const merged = { ...DEFAULT_DEPTH_MASK, ...(mask as Partial<DepthMaskOptions>) };\n return { ...merged, depthRange: [...merged.depthRange] as [number, number] };\n}\n\nconst ANNOTATION_TYPES = new Set(['pen', 'rect', 'circle', 'arrow', 'line', 'text', 'image']);\n\nconst isAnnotation = (value: unknown): value is Annotation =>\n isObject(value) &&\n typeof value.id === 'string' &&\n typeof value.type === 'string' &&\n ANNOTATION_TYPES.has(value.type) &&\n isNumber(value.x) &&\n isNumber(value.y);\n\n/**\n * Loads stored image editor state (a JSON string or object) with a `LoadReport`:\n *\n * - Runs `imageEditorStateMigrations` up to `IMAGE_EDITOR_STATE_VERSION`; a missing\n * `version` is taken as current.\n * - A newer `version` is still loaded but reported `readOnly`: do not save over it.\n * - Each section (`crop`, `transform`, `adjustments`, `depthMask`, `annotations`, and the\n * optional run-length encoded `selection`)\n * falls back to its default on its own; invalid annotations are dropped one by one.\n * - Unknown top-level keys are returned in `unknown`.\n *\n * Valid state from `toJSON()` comes back unchanged, so `loadJSON(state)` behaves\n * exactly as with the raw object.\n */\nexport function loadImageEditorDocument(input: unknown): ImageEditorDocumentLoad {\n let parsed: unknown = input;\n if (typeof input === 'string') {\n try {\n parsed = JSON.parse(input);\n } catch {\n parsed = input;\n }\n }\n if (isObject(parsed) && parsed.version === 3) return loadLayeredDocument(parsed);\n // Version 2 only adds `adjustments.layers`, which the version 1 sections keep as stored.\n const isV2 = isObject(parsed) && parsed.version === 2;\n if (isV2) parsed = { ...(parsed as Record<string, unknown>), version: 1 };\n const newer = isObject(parsed) && typeof parsed.version === 'number' && parsed.version > IMAGE_EDITOR_STATE_VERSION;\n let dropped = 0;\n const result = loadDocument<Omit<LegacySerializedImageEditorState, 'version'>>(\n parsed,\n {\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n adjustments: {\n parse: parseAdjustments,\n // Same as what `loadJSON` derives from missing adjustments.\n defaults: () => normalizeStoredAdjustments(undefined),\n },\n depthMask: {\n parse: parseDepthMask,\n defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] as [number, number] }),\n },\n annotations: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('annotations must be an array');\n const kept = value.filter(isAnnotation);\n dropped = value.length - kept.length;\n return JSON.parse(JSON.stringify(kept)) as Annotation[];\n },\n defaults: () => [],\n },\n // Optional: only present when something was selected when saving.\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n {\n // Versions 1 and 2 load as one schema; anything newer than 3 is reported read-only.\n currentVersion: newer ? IMAGE_EDITOR_STATE_VERSION : 1,\n steps: imageEditorStateMigrations,\n },\n );\n if (dropped > 0) {\n result.report.problems.push({\n section: 'annotations',\n message: `${dropped} invalid annotation${dropped === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 1, ...result.value } as LegacySerializedImageEditorState;\n if (isV2) {\n if (state.adjustments.layers?.length) state.version = 2;\n result.report.version = 2;\n }\n if (state.selection === undefined) delete state.selection;\n return { state, report: result.report, unknown: result.unknown };\n}\n\nconst LAYER_TYPES = new Set(['pixel', 'adjustment', 'group', 'annotation', 'smart']);\n\n/** Keeps layers that have an id-like shape and a known type (children checked too); counts the rest. */\nfunction cleanLayers(value: unknown[], dropped: { count: number }): SerializedLayer[] {\n const out: SerializedLayer[] = [];\n for (const raw of value) {\n if (!isObject(raw) || typeof raw.type !== 'string' || !LAYER_TYPES.has(raw.type)) {\n dropped.count++;\n continue;\n }\n if (raw.type === 'group') {\n const children = Array.isArray(raw.children) ? cleanLayers(raw.children, dropped) : [];\n out.push({ ...(raw as unknown as SerializedLayer), children } as SerializedLayer);\n } else out.push(JSON.parse(JSON.stringify(raw)) as SerializedLayer);\n }\n return out;\n}\n\n/** Version 3 (layers): each section falls back on its own; unreadable layers are dropped one by one. */\nfunction loadLayeredDocument(doc: Record<string, unknown>): ImageEditorDocumentLoad {\n const dropped = { count: 0 };\n const result = loadDocument<Omit<SerializedLayeredImageEditorState, 'version'>>(\n doc,\n {\n width: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('width must be a positive number'); })()), defaults: 0 },\n height: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('height must be a positive number'); })()), defaults: 0 },\n layers: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('layers must be an array');\n return cleanLayers(value, dropped);\n },\n defaults: () => [],\n },\n assets: {\n parse: (value) => {\n const assets = expectObject(value, 'assets');\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(assets)) if (typeof v === 'string') out[k] = v;\n return out;\n },\n defaults: () => ({}),\n },\n activeLayerId: { parse: (v) => (v === null || typeof v === 'string' ? v : null), defaults: null },\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n opacity: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('opacity must be a number');\n return Math.min(1, Math.max(0, v));\n },\n defaults: 1,\n },\n legacyOutputBlur: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('legacyOutputBlur must be a number');\n return Math.max(0, v);\n },\n defaults: undefined,\n },\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n smartContents: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('smartContents must be an array');\n // Entries are checked one by one when the layers load; keep the ones with an id.\n return value.filter((c) => isObject(c) && typeof c.id === 'string').map((c) => JSON.parse(JSON.stringify(c)));\n },\n defaults: undefined,\n },\n },\n { currentVersion: 3, steps: {} },\n );\n if (dropped.count > 0) {\n result.report.problems.push({\n section: 'layers',\n message: `${dropped.count} invalid layer${dropped.count === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 3, ...result.value } as SerializedLayeredImageEditorState;\n if (state.selection === undefined) delete state.selection;\n if (state.legacyOutputBlur === undefined) delete state.legacyOutputBlur;\n if (state.smartContents === undefined) delete state.smartContents;\n return { state, report: result.report, unknown: result.unknown };\n}\n","// Portions ported from lightcraft-main/crates/develop/src/controls.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentKind, ImageAdjustments } from './types';\n\n/**\n * Control specs: one table describes every numeric adjustment slider (id, label, section, range,\n * default, step, display precision and track style). The React and Svelte panels render from it,\n * the agent tools expose it as a schema, and values are addressed by id.\n */\n\nexport type ControlSection = 'light' | 'color' | 'effects' | 'layer';\n\n/** How a slider track is drawn. */\nexport type ControlTrack =\n /** Plain track, fill from the minimum. */\n | { kind: 'plain' }\n /** Zero in the middle. */\n | { kind: 'centered' }\n /** Blue → yellow (white-balance temperature). */\n | { kind: 'temp' }\n /** Green → magenta (tint). */\n | { kind: 'tint' }\n /** A two-colour gradient. */\n | { kind: 'gradient'; from: string; to: string }\n /** Full hue rainbow. */\n | { kind: 'rainbow' };\n\nexport interface ControlSpec {\n /** Dotted path of the value (`exposure`; inside a layer's params: `midtones.0`, `ranges.4.hue`). */\n id: string;\n label: string;\n section: ControlSection;\n min: number;\n max: number;\n step: number;\n default: number;\n /** Decimal places shown (after `displayScale`). */\n decimals: number;\n track: ControlTrack;\n /** Multiplier for display (opacity 0..1 shows as 0..100 %). */\n displayScale?: number;\n /** Unit suffix for display (`px`, `%`, `°`). */\n unit?: string;\n /** The rendering process the control needs (tint: process 2). */\n requiresProcess?: 1 | 2;\n}\n\nconst plain: ControlTrack = { kind: 'plain' };\nconst centered: ControlTrack = { kind: 'centered' };\n\n/** Every numeric base adjustment, in panel order. */\nexport const IMAGE_ADJUSTMENT_CONTROLS: readonly ControlSpec[] = [\n { id: 'brightness', label: 'Brightness', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'contrast', label: 'Contrast', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n {\n id: 'saturation', label: 'Saturation', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' },\n },\n { id: 'exposure', label: 'Exposure', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'temperature', label: 'Warmth / Temperature', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'temp' } },\n { id: 'tint', label: 'Tint', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'tint' }, requiresProcess: 2 },\n { id: 'blur', label: 'Blur', section: 'effects', min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: 'px' },\n { id: 'opacity', label: 'Opacity', section: 'effects', min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\nconst tone = (prefix: string, label: string): ControlSpec[] =>\n [\n ['0', 'Cyan – Red', '#00b7c7', '#e0343a'],\n ['1', 'Magenta – Green', '#d23ad2', '#2fb34a'],\n ['2', 'Yellow – Blue', '#e0c63a', '#3a5be0'],\n ].map(([i, l, from, to]) => ({\n id: `${prefix}.${i}`, label: `${label} ${l}`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from, to },\n }));\n\nconst levelsRecord = (ch: string, label: string): ControlSpec[] => [\n { id: `${ch}.inBlack`, label: `${label} input black`, section: 'layer', min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.gamma`, label: `${label} gamma`, section: 'layer', min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },\n { id: `${ch}.inWhite`, label: `${label} input white`, section: 'layer', min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n { id: `${ch}.outBlack`, label: `${label} output black`, section: 'layer', min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.outWhite`, label: `${label} output white`, section: 'layer', min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n];\n\nconst RANGE_NAMES = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'];\n\n/**\n * Numeric parameters of each adjustment-layer kind, addressed inside `layer.params`. Curves points\n * and booleans / colours (colorize, preserve luminosity, tint, filter colour) are edited by the\n * dedicated controls, not sliders.\n */\nexport const ADJUSTMENT_LAYER_CONTROLS: Readonly<Record<AdjustmentKind, readonly ControlSpec[]>> = {\n curves: [],\n levels: [\n ...levelsRecord('master', 'RGB'),\n ...levelsRecord('red', 'Red'),\n ...levelsRecord('green', 'Green'),\n ...levelsRecord('blue', 'Blue'),\n ],\n hueSaturation: [\n { id: 'hue', label: 'Hue', section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n { id: 'lightness', label: 'Lightness', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } },\n ...RANGE_NAMES.flatMap((name, i): ControlSpec[] => [\n { id: `ranges.${i}.hue`, label: `${name} hue`, section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: `ranges.${i}.saturation`, label: `${name} saturation`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: `ranges.${i}.lightness`, label: `${name} lightness`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n ]),\n ],\n vibrance: [\n { id: 'vibrance', label: 'Vibrance', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#d8406a' } },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n ],\n colorBalance: [...tone('shadows', 'Shadows'), ...tone('midtones', 'Midtones'), ...tone('highlights', 'Highlights')],\n blackWhite: [\n ['reds', 'Reds', 40, '#e0343a'],\n ['yellows', 'Yellows', 60, '#e0c63a'],\n ['greens', 'Greens', 40, '#2fb34a'],\n ['cyans', 'Cyans', 60, '#00b7c7'],\n ['blues', 'Blues', 20, '#3a5be0'],\n ['magentas', 'Magentas', 80, '#d23ad2'],\n ].map(([id, label, def, to]) => ({\n id: id as string, label: label as string, section: 'layer' as const, min: -200, max: 300, step: 1, default: def as number, decimals: 0,\n track: { kind: 'gradient' as const, from: '#000000', to: to as string }, unit: '%',\n })),\n invert: [],\n posterize: [{ id: 'levels', label: 'Levels', section: 'layer', min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],\n threshold: [{ id: 'level', label: 'Threshold level', section: 'layer', min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } }],\n photoFilter: [{ id: 'density', label: 'Density', section: 'layer', min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: '%' }],\n};\n\n/** Opacity of any adjustment layer (0..1, shown as %). */\nexport const ADJUSTMENT_LAYER_OPACITY_CONTROL: ControlSpec = {\n id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%',\n};\n\nconst BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));\n\n/** The base-adjustment control with this id. */\nexport function findAdjustmentControl(id: string): ControlSpec | undefined {\n return BASE_BY_ID.get(id);\n}\n\n/** The control of a layer parameter (`opacity` included). */\nexport function findLayerControl(kind: AdjustmentKind, id: string): ControlSpec | undefined {\n if (id === 'opacity') return ADJUSTMENT_LAYER_OPACITY_CONTROL;\n return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);\n}\n\n/** `v` clamped to the control's range and snapped to its step; non-finite values give the default. */\nexport function clampControlValue(spec: ControlSpec, v: unknown): number {\n if (typeof v !== 'number' || !Number.isFinite(v)) return spec.default;\n const clamped = Math.min(spec.max, Math.max(spec.min, v));\n const steps = Math.round((clamped - spec.min) / spec.step);\n const snapped = spec.min + steps * spec.step;\n // Trim float noise from the step arithmetic.\n return Math.min(spec.max, Number(snapped.toFixed(6)));\n}\n\n/** A value as the slider shows it (`+12`, `35%`, `4px`). */\nexport function formatControlValue(spec: ControlSpec, v: number): string {\n const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);\n const s = shown.toFixed(spec.decimals);\n return `${s}${spec.unit ?? ''}`;\n}\n\n/** Where `v` sits on the track, 0 (left) to 1 (right). */\nexport function controlPosition(spec: ControlSpec, v: number): number {\n const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);\n return Math.min(1, Math.max(0, p));\n}\n\n/** CSS background for a control's track. */\nexport function controlTrackBackground(spec: ControlSpec): string | undefined {\n const t = spec.track;\n switch (t.kind) {\n case 'temp':\n return 'linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)';\n case 'tint':\n return 'linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)';\n case 'gradient':\n return `linear-gradient(to right, ${t.from}, ${t.to})`;\n case 'rainbow':\n return 'linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)';\n default:\n return undefined;\n }\n}\n\n/** Reads a dotted path (`ranges.2.hue`) from an object. */\nexport function getControlPath(obj: unknown, path: string): unknown {\n let cur: any = obj;\n for (const part of path.split('.')) {\n if (cur === null || cur === undefined) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\n/** A copy of `obj` with the dotted path set (arrays and objects along the path are cloned). */\nexport function setControlPath<T>(obj: T, path: string, value: unknown): T {\n const parts = path.split('.');\n const root: any = Array.isArray(obj) ? [...(obj as unknown[])] : { ...(obj as object) };\n let cur = root;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = cur[parts[i]];\n cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === 'object' ? { ...next } : {};\n cur = cur[parts[i]];\n }\n cur[parts[parts.length - 1]] = value;\n return root as T;\n}\n\n/** The base adjustment values the control table describes. */\nexport function readAdjustmentControls(adjustments: ImageAdjustments): Record<string, number> {\n const out: Record<string, number> = {};\n for (const c of IMAGE_ADJUSTMENT_CONTROLS) {\n const v = (adjustments as unknown as Record<string, unknown>)[c.id];\n out[c.id] = typeof v === 'number' && Number.isFinite(v) ? v : c.default;\n }\n return out;\n}\n\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 'smart':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.contents = { id: layer.contents.id, name: layer.contents.name, kind: layer.contents.kind, width: layer.contents.width, height: layer.contents.height };\n out.smartFilters = (layer.smartFilters?.filters ?? []).map((f) => ({ id: f.id, kind: f.kind, enabled: f.enabled }));\n out.filterMask = maskSummary(layer.smartFilters?.mask);\n break;\n }\n return out;\n}\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers.' } as const;\nconst PLACEMENT = {\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (same group).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer (same group).' },\n parentId: { type: 'string', description: 'Put the layer inside this group (at index, default its top).' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom (with parentId, or at the top level).' },\n} as const;\nconst BLEND = { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' } as const;\n\nfunction placementFrom(args: Record<string, unknown>): LayerPlacement {\n const p: LayerPlacement = {};\n if (typeof args.aboveId === 'string') p.aboveId = args.aboveId;\n if (typeof args.belowId === 'string') p.belowId = args.belowId;\n if (typeof args.parentId === 'string') p.parentId = args.parentId;\n if (typeof args.index === 'number') p.index = args.index;\n return p;\n}\n\n/**\n * Agent tools for the image editor's layer stack: `list_layers`, `set_active_layer`,\n * `add_pixel_layer`, `duplicate_layer`, `remove_layer`, `move_layer`, `set_layer_properties`,\n * `group_layers`, `ungroup_layer`, `merge_down`, `merge_visible`, `flatten_image`, `offset_layer`,\n * `add_layer_mask`, `apply_layer_mask`, `remove_layer_mask`, `set_layer_mask_properties`,\n * `layer_via_copy`, `add_layer_filter`, `update_layer_filter` and `remove_layer_filter`. Each write\n * is one undo step, validates before changing anything and returns what it changed.\n */\nexport function createImageLayerTools(controller: ImageEditorController): AgentTool[] {\n const describe = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n if (!loc) return null;\n return {\n ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)),\n parentId: loc.parent?.id ?? null,\n index: loc.index,\n };\n };\n const exists = (id: unknown, what = 'layerId') => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError(`${what} is required (a layer id from list_layers).`);\n requireLayer(controller.getLayers(), id);\n return id;\n };\n\n const listLayers: AgentTool = {\n name: 'list_layers',\n title: 'List image layers',\n description:\n 'Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List image layers', ...READ },\n handler: () => {\n const s = controller.getState();\n return {\n canvas: { ...s.imageDimensions },\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n };\n },\n };\n\n const setActive: AgentTool = {\n name: 'set_active_layer',\n title: 'Set the active layer',\n description:\n 'Makes a layer active (the base sliders of set_adjustments then edit its develop filter when it is a pixel layer) and chooses whether painting targets its pixels or its mask. View state, not an undo step. Returns the active layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Layer to activate (from list_layers).' },\n target: { type: 'string', enum: ['pixels', 'mask'], description: 'Paint target (default pixels; mask needs a mask).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the active layer', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, target }) => {\n const id = exists(layerId);\n const layer = controller.getLayer(id)!;\n if (target === 'mask' && !layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n controller.setActiveLayer(id, (target as 'pixels' | 'mask' | undefined) ?? 'pixels');\n return { layer: describe(id), activeTarget: controller.getState().activeTarget };\n },\n };\n\n const addPixelLayer: AgentTool = {\n name: 'add_pixel_layer',\n title: 'Add a pixel layer',\n description:\n 'Adds a pixel layer and makes it active: empty (default), filled with color over the canvas, or holding an image (PNG data URL or base64 PNG; other formats and URLs only in a browser) placed at x, y. Goes above the active layer unless aboveId / belowId / parentId / index is given. One undo step. Returns the new layer with its id, bounds and position.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Layer name (default \"Layer N\").' },\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa; omit for an empty layer.' },\n image: { type: 'string', description: 'Image to place: PNG data URL or base64 PNG (or any URL in a browser).' },\n x: { type: 'integer', description: 'Left of the image in document pixels (default 0).' },\n y: { type: 'integer', description: 'Top of the image in document pixels (default 0).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1 (default 1).' },\n blend: BLEND,\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a pixel layer', ...WRITE },\n handler: async (args) => {\n if (args.color !== undefined && args.image !== undefined) throw new LayerOperationError('Give color or image, not both.');\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n const init = {\n name: args.name as string | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n ...placementFrom(args),\n };\n const id =\n typeof args.image === 'string'\n ? await controller.addPixelLayerFromImage(args.image, init)\n : controller.addPixelLayer({ ...init, color: args.color as string | undefined });\n return { layer: describe(id) };\n },\n };\n\n const duplicate: AgentTool = {\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Duplicates a layer (groups with their children) directly above it and makes the copy active. One undo step. Returns the copy with its new id.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId, name }) => ({ layer: describe(controller.duplicateLayer(exists(layerId), name as string | undefined)) }),\n };\n\n const remove: AgentTool = {\n name: 'remove_layer',\n title: 'Delete a layer',\n description:\n 'Deletes a layer (a group with its children). Refused for a layer locked with all. Prefer hiding it (set_layer_properties visible false) when it may be wanted back. One undo step. Returns the removed id and the remaining top-level ids.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayer(id);\n return { removed: id, layers: controller.getLayers().map((l) => l.id) };\n },\n };\n\n const move: AgentTool = {\n name: 'move_layer',\n title: 'Move a layer in the stack',\n description:\n 'Moves a layer above or below another layer (aboveId / belowId, in that layer\\'s group), into a group (parentId, optional index), or to an index at the top level. Give exactly one of aboveId, belowId, parentId, or index alone. One undo step. Returns the new parentId and index.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, ...PLACEMENT },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer in the stack', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n return { layerId: id, ...controller.moveLayer(id, placementFrom(args)) };\n },\n };\n\n const setProps: AgentTool = {\n name: 'set_layer_properties',\n title: 'Set layer properties',\n description:\n 'Changes a layer\\'s name, visible, opacity (0..1), fillOpacity (0..1, pixel layers: fades the pixels but not masks), blend (pass-through is for groups), clipped (clip to the layer below), locks ({pixels, transparency, position, all}) or collapsed (groups). Only the given fields change. Refused (except visible, locks and collapsed) on a layer locked with all. One undo step. Returns the layer after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n name: { type: 'string', description: 'New name.' },\n visible: { type: 'boolean', description: 'Show (true) or hide (false).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n fillOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill 0..1 (pixel layers).' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode; pass-through for groups only.' },\n clipped: { type: 'boolean', description: 'Clip to the nearest unclipped layer below.' },\n locks: {\n type: 'object',\n description: 'Locks to set (missing keys are unlocked).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting and filling.' },\n transparency: { type: 'boolean', description: 'Painting keeps alpha.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'Everything, including properties and deletion.' },\n },\n additionalProperties: false,\n },\n collapsed: { type: 'boolean', description: 'Collapse a group in the layers panel.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer properties', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['name', 'visible', 'opacity', 'fillOpacity', 'blend', 'clipped', 'locks', 'collapsed']) if (args[k] !== undefined) props[k] = args[k];\n if (!Object.keys(props).length) throw new LayerOperationError('Give at least one property to change.');\n controller.setLayerProperties(id, props);\n return { layer: describe(id) };\n },\n };\n\n const group: AgentTool = {\n name: 'group_layers',\n title: 'Group layers',\n description:\n 'Puts sibling layers (same group or top level) into a new pass-through group at the position of the topmost one, keeping their order. One undo step. Returns the group with its id and children.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Ids of the layers to group (from list_layers).' },\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n },\n required: ['layerIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Group layers', ...WRITE },\n handler: ({ layerIds, name }) => {\n const ids = (layerIds as string[]).map((id) => exists(id, 'layerIds[]'));\n return { group: describe(controller.groupLayers(ids, name as string | undefined)) };\n },\n };\n\n const addGroup: AgentTool = {\n name: 'add_group',\n title: 'Add an empty group',\n description:\n 'Adds an empty group (pass-through unless blend is given) above the active layer, or at aboveId / belowId / parentId / index, and makes it active. Move layers into it with move_layer parentId, or use group_layers to group existing layers. One undo step. Returns the group.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (default pass-through).' },\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add an empty group', ...WRITE },\n handler: (args) => {\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n return { group: describe(controller.addGroup({ name: args.name as string | undefined, blend: args.blend as never, ...placementFrom(args) })) };\n },\n };\n\n const ungroup: AgentTool = {\n name: 'ungroup_layer',\n title: 'Ungroup a group',\n description: 'Replaces a group by its children at its position (the group\\'s own opacity, blend and mask are dropped). One undo step. Returns the child ids bottom to top.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'Group id.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Ungroup a group', ...WRITE },\n handler: ({ layerId }) => ({ childIds: controller.ungroupLayer(exists(layerId)) }),\n };\n\n const mergeDown: AgentTool = {\n name: 'merge_down',\n title: 'Merge a layer down',\n description:\n 'Composites a layer (pixel, adjustment, group or annotation) onto the pixel layer directly below it. The lower layer keeps its name, opacity and blend; masks, fill and filters are baked into its pixels. Refused when the layer below is not a pixel layer or is locked, or when the layer is hidden. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'The upper layer to merge down.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.mergeDown(exists(layerId))) }),\n };\n\n const mergeVisible: AgentTool = {\n name: 'merge_visible',\n title: 'Merge visible layers',\n description: 'Composites every visible top-level layer into one normal pixel layer at the position of the lowest of them; hidden layers stay. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Merge visible layers', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.mergeVisible()) }),\n };\n\n const flatten: AgentTool = {\n name: 'flatten_image',\n title: 'Flatten the image',\n description: 'Composites every visible layer into one Background pixel layer and discards hidden layers. Crop, rotation and output opacity stay. One undo step. Returns the Background layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Flatten the image', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.flattenImage()) }),\n };\n\n const offset: AgentTool = {\n name: 'offset_layer',\n title: 'Move a layer\\'s pixels',\n description:\n 'Moves a pixel layer (and its mask) to an absolute offset x, y or by dx, dy, in document pixels (integers). Refused when its position is locked. One undo step. Returns the new offset and document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n x: { type: 'integer', description: 'New horizontal offset in document pixels.' },\n y: { type: 'integer', description: 'New vertical offset in document pixels.' },\n dx: { type: 'integer', description: 'Move right by this many pixels (negative: left).' },\n dy: { type: 'integer', description: 'Move down by this many pixels (negative: up).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer\\'s pixels', ...WRITE },\n handler: ({ layerId, x, y, dx, dy }) => {\n const id = exists(layerId);\n if ([x, y, dx, dy].every((v) => v === undefined)) throw new LayerOperationError('Give x and y, or dx and dy.');\n if ((x !== undefined || y !== undefined) && (dx !== undefined || dy !== undefined)) throw new LayerOperationError('Give x / y or dx / dy, not both.');\n return { layerId: id, ...controller.offsetLayer(id, { x: x as number | undefined, y: y as number | undefined, dx: dx as number | undefined, dy: dy as number | undefined }) };\n },\n };\n\n const addMask: AgentTool = {\n name: 'add_layer_mask',\n title: 'Add a layer mask',\n description:\n 'Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection\\'s coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection'], description: 'What the mask starts as (default reveal-all).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px (default 0).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer mask', ...WRITE },\n handler: ({ layerId, from, density, feather }) => {\n const id = exists(layerId);\n const mask = controller.addLayerMask(id, (from as 'reveal-all' | 'hide-all' | 'selection' | undefined) ?? 'reveal-all', {\n density: density as number | undefined,\n feather: feather as number | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_layer_mask',\n title: 'Apply a layer mask',\n description: \"Bakes a pixel layer's mask into its pixels (alpha × coverage, including density and feather) and removes the mask. Pixel layers only; refused when its pixels are locked. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Apply a layer mask', ...DESTRUCTIVE },\n handler: async ({ layerId }) => {\n const id = exists(layerId);\n await controller.applyLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const removeMask: AgentTool = {\n name: 'remove_layer_mask',\n title: 'Delete a layer mask',\n description: \"Deletes a layer's mask without applying it (the layer shows everywhere again). Disable it instead with set_layer_mask_properties enabled false to keep it. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer mask', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const maskProps: AgentTool = {\n name: 'set_layer_mask_properties',\n title: 'Set layer mask properties',\n description:\n \"Changes a layer mask's density (0..1), feather (px), enabled flag or linked flag; its pixels are kept. An unlinked mask (linked false) stays in place when the layer is moved, transformed or warped; transform it alone with transform_layer target mask. One undo step. Returns the mask summary.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Density 0..1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px.' },\n enabled: { type: 'boolean', description: 'false ignores the mask but keeps it.' },\n linked: { type: 'boolean', description: 'false unlinks the mask from the layer pixels (it stays put when they move or transform); true links it again.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer mask properties', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, density, feather, enabled, linked }) => {\n const id = exists(layerId);\n if ([density, feather, enabled, linked].every((v) => v === undefined)) throw new LayerOperationError('Give density, feather, enabled or linked.');\n const mask = controller.setLayerMaskProps(id, {\n density: density as number | undefined,\n feather: feather as number | undefined,\n enabled: enabled as boolean | undefined,\n linked: linked as boolean | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const viaCopy: AgentTool = {\n name: 'layer_via_copy',\n title: 'New layer from the selection',\n description:\n 'Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Pixel layer to copy from (default the active layer).' },\n cut: { type: 'boolean', description: 'Remove the pixels from the source too (default false).' },\n name: { type: 'string', description: 'Name of the new layer.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'New layer from the selection', ...WRITE },\n handler: ({ layerId, cut, name }) => {\n if (layerId !== undefined) exists(layerId);\n return { layer: describe(controller.layerViaCopy({ layerId: layerId as string | undefined, cut: cut === true, name: name as string | undefined })) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_layer',\n title: 'Rasterize a layer',\n description:\n 'Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer\\'s filters (develop, blur) into its pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a layer', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) }),\n };\n\n const filterArgs = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it.' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n } as const;\n const filterValues = (args: Record<string, unknown>) => {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(filterArgs)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n };\n\n const addFilter: AgentTool = {\n name: 'add_layer_filter',\n title: 'Add a layer filter',\n description:\n 'Adds a live filter to a pixel layer: develop (the base sliders: brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px). One of each kind per layer, applied bottom to top before the mask. One undo step. Returns the layer\\'s filters with their ids.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' }, ...filterArgs },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer filter', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n const filters = controller.addLayerFilter(id, { kind: args.kind as LayerFilter['kind'], ...filterValues(args) } as never);\n return { layerId: id, filters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_layer_filter',\n title: 'Change a layer filter',\n description: \"Changes one filter of a pixel layer (only the given values; the kind cannot change). One undo step. Returns the layer's filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers or add_layer_filter.' }, ...filterArgs },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const changes = filterValues(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n return { layerId: id, filters: controller.updateLayerFilter(id, args.filterId as string, changes) };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_layer_filter',\n title: 'Remove a layer filter',\n description: \"Removes one filter from a pixel layer. One undo step. Returns the layer's remaining filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers.' } },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Remove a layer filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = exists(layerId);\n return { layerId: id, filters: controller.removeLayerFilter(id, filterId as string) };\n },\n };\n\n return [\n listLayers,\n setActive,\n addPixelLayer,\n duplicate,\n remove,\n move,\n setProps,\n group,\n addGroup,\n ungroup,\n mergeDown,\n mergeVisible,\n flatten,\n offset,\n addMask,\n applyMask,\n removeMask,\n maskProps,\n viaCopy,\n rasterize,\n addFilter,\n updateFilter,\n removeFilter,\n ];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES } from './blend';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport {\n effectsMargin,\n isLayerEffectKind,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type LayerEffectKind,\n type LayerEffects,\n} from '../raster/effects/model';\nimport { builtinPattern, builtinPatternIds } from '../raster/effects/patterns';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers (a pixel, annotation or group layer).' } as const;\nconst EFFECT_ID = { type: 'string', description: 'Effect id from list_layer_effects or add_layer_effect.' } as const;\nconst BLEND_IDS = BLEND_MODES.map((m) => m.id).filter((id) => id !== 'pigment');\n\n/** Shared shapes, referenced as `#/$defs/...`. */\nconst DEFS = {\n color: { type: 'string', pattern: '^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', description: 'Colour as #rrggbb.' },\n contour: {\n type: 'object',\n description: 'A transfer curve over the effect coverage: points { input, output } from 0 to 1 (at least 2), sorted by input. Linear is [{0,0},{1,1}].',\n properties: {\n name: { type: 'string', description: 'Display name (e.g. \"Cone\", \"Gaussian\").' },\n points: {\n type: 'array',\n minItems: 2,\n description: 'Curve points.',\n items: {\n type: 'object',\n properties: {\n input: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage in (0..1).' },\n output: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage out (0..1).' },\n corner: { type: 'boolean', description: 'Sharp corner instead of a smooth point (default false).' },\n },\n required: ['input', 'output'],\n additionalProperties: false,\n },\n },\n },\n required: ['name', 'points'],\n additionalProperties: false,\n },\n gradient: {\n type: 'object',\n description: 'A gradient: colour stops, optional opacity stops and its layout.',\n properties: {\n stops: {\n type: 'array',\n minItems: 1,\n description: 'Colour stops { position 0..1, color #rrggbb }.',\n items: {\n type: 'object',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, color: { $ref: '#/$defs/color' } },\n required: ['position', 'color'],\n additionalProperties: false,\n },\n },\n opacityStops: {\n type: 'array',\n description: 'Opacity stops { position 0..1, opacity 0..1 } (default opaque).',\n items: {\n type: 'object',\n properties: {\n position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1.' },\n },\n required: ['position', 'opacity'],\n additionalProperties: false,\n },\n },\n style: { type: 'string', enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'], description: 'Geometry.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees, 90 = bottom to top.' },\n scale: { type: 'number', minimum: 0.1, maximum: 1.5, description: '1 = 100 %.' },\n reverse: { type: 'boolean', description: 'Swap the ends.' },\n align: { type: 'boolean', description: 'Lay out over the layer bounds (true) or the canvas.' },\n offset: {\n type: 'object',\n description: 'Centre offset as fractions of the bounds.',\n properties: { x: { type: 'number', minimum: -1, maximum: 1, description: 'Fraction of the width.' }, y: { type: 'number', minimum: -1, maximum: 1, description: 'Fraction of the height.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n required: ['stops', 'style', 'angle', 'scale', 'reverse', 'align', 'offset'],\n additionalProperties: false,\n },\n phase: {\n type: 'object',\n description: 'Pattern origin offset in px.',\n properties: { x: { type: 'number', description: 'px.' }, y: { type: 'number', description: 'px.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n fill: {\n description: 'What a stroke or glow paints with. Glows take color or gradient only.',\n anyOf: [\n {\n type: 'object',\n properties: { type: { const: 'color', description: 'Solid colour.' }, color: { $ref: '#/$defs/color' } },\n required: ['type', 'color'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: { type: { const: 'gradient', description: 'Gradient.' }, gradient: { $ref: '#/$defs/gradient' } },\n required: ['type', 'gradient'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n type: { const: 'pattern', description: 'Pattern (strokes only).' },\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Rotation in degrees (default 0).' },\n link: { type: 'boolean', description: 'Tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['type', 'patternId', 'scale'],\n additionalProperties: false,\n },\n ],\n },\n} as const;\n\nconst nullable = (schema: Record<string, unknown>, description: string) => ({ anyOf: [schema, { type: 'null' }], description });\n\n/** Every effect setting (each kind takes a subset; the description says which). */\nfunction settingsSchema(forUpdate: boolean): Record<string, unknown> {\n const opt = (schema: Record<string, unknown>, description: string) => (forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description });\n return {\n type: 'object',\n description: forUpdate\n ? 'Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect.'\n : \"Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.\",\n properties: {\n enabled: { type: 'boolean', description: 'All kinds: false keeps the effect but hides it (default true).' },\n blend: { type: 'string', enum: BLEND_IDS, description: 'All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1).' },\n color: { $ref: '#/$defs/color', description: 'drop-shadow, inner-shadow, satin, color-overlay: #rrggbb.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Shadows, satin, bevel-emboss: light direction in degrees, counter-clockwise from 3 o\\'clock (default 120; satin 19).' },\n useGlobalLight: { type: 'boolean', description: 'Shadows, bevel-emboss: follow the document global light instead of angle (default true).' },\n distance: { type: 'number', minimum: 0, maximum: 30000, description: 'Shadows, satin: offset in px (default 5; satin 11).' },\n spread: { type: 'number', minimum: 0, maximum: 1, description: 'drop-shadow, outer-glow: 0..1 of the size made solid (default 0).' },\n choke: { type: 'number', minimum: 0, maximum: 1, description: 'inner-shadow, inner-glow: 0..1 of the size made solid (default 0).' },\n size: { type: 'number', minimum: 0, maximum: 250, description: 'Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3).' },\n contour: opt({ $ref: '#/$defs/contour' }, 'Shadows, glows, satin: coverage contour (default linear).'),\n antiAlias: { type: 'boolean', description: 'Shadows, glows, satin: smooth the contour (default false; satin true).' },\n noise: { type: 'number', minimum: 0, maximum: 1, description: 'Shadows, glows: speckle 0..1 (default 0).' },\n knocksOut: { type: 'boolean', description: 'drop-shadow: the layer hides the shadow beneath its see-through fill (default true).' },\n fill: { $ref: '#/$defs/fill', description: 'outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern).' },\n technique: { type: 'string', enum: ['softer', 'precise', 'smooth', 'chisel-hard', 'chisel-soft'], description: 'Glows: softer | precise (default softer); bevel-emboss: smooth | chisel-hard | chisel-soft (default smooth).' },\n range: { type: 'number', minimum: 0.01, maximum: 1, description: 'Glows: the part of the glow the contour spans (default 0.5).' },\n jitter: { type: 'number', minimum: 0, maximum: 1, description: 'Glows: gradient jitter 0..1 (stored, not rendered; default 0).' },\n source: { type: 'string', enum: ['edge', 'center'], description: 'inner-glow: glow from the edge (default) or the centre.' },\n style: { type: 'string', enum: ['inner-bevel', 'outer-bevel', 'emboss', 'pillow-emboss', 'stroke-emboss'], description: 'bevel-emboss: style (default inner-bevel).' },\n depth: { type: 'number', minimum: 0, maximum: 10, description: 'bevel-emboss: depth, 1 = 100 % (default 1).' },\n direction: { type: 'string', enum: ['up', 'down'], description: 'bevel-emboss: raised (up, default) or sunk (down).' },\n soften: { type: 'number', minimum: 0, maximum: 16, description: 'bevel-emboss: blur of the height in px (default 0).' },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'bevel-emboss: light altitude in degrees (default 30).' },\n glossContour: opt({ $ref: '#/$defs/contour' }, 'bevel-emboss: gloss contour over the shading (default linear).'),\n highlightColor: { $ref: '#/$defs/color', description: 'bevel-emboss: highlight colour (default #ffffff).' },\n highlightBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: highlight mode (default screen).' },\n highlightOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: highlight opacity (default 0.75).' },\n shadowColor: { $ref: '#/$defs/color', description: 'bevel-emboss: shadow colour (default #000000).' },\n shadowBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: shadow mode (default multiply).' },\n shadowOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: shadow opacity (default 0.75).' },\n contourElement: opt(\n {\n type: 'object',\n properties: {\n contour: { $ref: '#/$defs/contour' },\n range: { type: 'number', minimum: 0, maximum: 1, description: 'Stored range (default 0.5).' },\n antiAlias: { type: 'boolean', description: 'Smooth the contour.' },\n },\n required: ['contour', 'range', 'antiAlias'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Contour element, shaping the bevel's height profile (default off).\"\n ),\n texture: opt(\n {\n type: 'object',\n properties: {\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n depth: { type: 'number', minimum: -10, maximum: 10, description: '1 = +100 %; negative carves the pattern in.' },\n invert: { type: 'boolean', description: 'Invert the pattern.' },\n link: { type: 'boolean', description: 'Tile from the layer (default true).' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['patternId', 'scale', 'depth', 'invert'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Texture element, a pattern's luminance as extra height (default off).\"\n ),\n invert: { type: 'boolean', description: 'satin: invert (default true).' },\n gradient: { $ref: '#/$defs/gradient', description: 'gradient-overlay: the gradient (default black to white, 90°).' },\n dither: { type: 'boolean', description: 'gradient-overlay: dither (stored; default false).' },\n patternId: { type: 'string', description: 'pattern-overlay: pattern id from list_layer_effects.patterns (default checker).' },\n name: { type: 'string', description: 'pattern-overlay: pattern display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'pattern-overlay: 1 = 100 % (default 1).' },\n link: { type: 'boolean', description: 'pattern-overlay: tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase', description: 'pattern-overlay: origin offset in px (default 0, 0).' },\n position: { type: 'string', enum: ['outside', 'inside', 'center'], description: 'stroke: where the stroke sits on the edge (default outside).' },\n },\n additionalProperties: false,\n };\n}\n\n/** The patterns pattern effects can use: the document's (from a PSD), then the built-in ones. */\nfunction patternList(controller: ImageEditorController): Array<{ id: string; name: string; width: number; height: number; builtin: boolean }> {\n const doc = controller.getDocumentPatterns().map((p) => ({ id: p.id, name: p.name, width: p.width, height: p.height, builtin: false }));\n const builtins = builtinPatternIds().map((id) => {\n const p = builtinPattern(id)!;\n return { id, name: p.name, width: p.width, height: p.height, builtin: true };\n });\n return [...doc, ...builtins];\n}\n\n/** A layer's effects for agents: the master switch, scale, reach and every effect with its settings. */\nexport function describeLayerEffects(effects: LayerEffects): Record<string, unknown> {\n return {\n enabled: effects.enabled,\n scale: effects.scale ?? 1,\n margin: effectsMargin(effects),\n fromPsd: Boolean(effects.psd),\n effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) })),\n };\n}\n\n/**\n * Agent tools for layer effects: `list_layer_effects`, `add_layer_effect`, `update_layer_effect`,\n * `remove_layer_effect`, `set_layer_effects_enabled` and `set_global_light`. Each write validates\n * every setting first, is one undo step and returns the layer's effects after the change.\n */\nexport function createImageEffectTools(controller: ImageEditorController): AgentTool[] {\n const layerArg = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a layer id from list_layers).');\n requireLayer(controller.getLayers(), id);\n return id;\n };\n const result = (layerId: string, extra: Record<string, unknown> = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });\n\n const list: AgentTool = {\n name: 'list_layer_effects',\n title: 'List layer effects',\n description:\n \"Returns a layer's effects: the master switch (enabled), Scale Effects (scale, 1 = 100 %), margin (px the effects reach beyond the shape), whether they came from a PSD, and every effect with its id, kind, label and all settings. Also returns the document global light and the pattern ids pattern effects can use.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'List layer effects', ...READ },\n handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] }),\n };\n\n const add: AgentTool = {\n name: 'add_layer_effect',\n title: 'Add a layer effect',\n description:\n \"Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: [...LAYER_EFFECT_KINDS], description: 'Effect kind.' },\n settings: settingsSchema(false),\n index: { type: 'integer', minimum: 0, description: \"Position in the layer's effect list (default the end).\" },\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer effect', ...WRITE },\n handler: ({ layerId, kind, settings, index }) => {\n const id = layerArg(layerId);\n if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(', ')}.`);\n const { effect } = controller.addLayerEffect(id, kind as LayerEffectKind, (settings as Record<string, unknown>) ?? {}, index as number | undefined);\n return result(id, { effectId: effect.id });\n },\n };\n\n const update: AgentTool = {\n name: 'update_layer_effect',\n title: 'Change a layer effect',\n description:\n \"Changes settings of one effect (only the given ones; fill, gradient, contour and texture objects are replaced whole, null removes an optional contour or texture). The kind cannot change. Validates everything before changing anything. One undo step. Returns the layer's effects.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: { layerId: LAYER_ID, effectId: EFFECT_ID, settings: settingsSchema(true) },\n required: ['layerId', 'effectId', 'settings'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer effect', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, effectId, settings }) => {\n const id = layerArg(layerId);\n const changes = (settings as Record<string, unknown>) ?? {};\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one setting to change.');\n controller.updateLayerEffect(id, effectId as string, changes);\n return result(id);\n },\n };\n\n const remove: AgentTool = {\n name: 'remove_layer_effect',\n title: 'Remove a layer effect',\n description: \"Removes one effect from a layer (to hide it but keep its settings, set enabled false with update_layer_effect instead). One undo step. Returns the layer's remaining effects.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, effectId: EFFECT_ID }, required: ['layerId', 'effectId'], additionalProperties: false },\n annotations: { title: 'Remove a layer effect', ...DESTRUCTIVE },\n handler: ({ layerId, effectId }) => {\n const id = layerArg(layerId);\n controller.removeLayerEffect(id, effectId as string);\n return result(id);\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_layer_effects_enabled',\n title: 'Switch layer effects on or off',\n description:\n \"Turns all of a layer's effects on or off at once (the master switch; each effect keeps its own enabled flag) and optionally sets Scale Effects, which multiplies every size, distance and pattern scale. One undo step. Returns the layer's effects.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n enabled: { type: 'boolean', description: 'true renders the enabled effects; false hides them all.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'Scale Effects, 1 = 100 % (unchanged when left out).' },\n },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Switch layer effects on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled, scale }) => {\n const id = layerArg(layerId);\n controller.setLayerEffectsEnabled(id, enabled as boolean, scale as number | undefined);\n return result(id);\n },\n };\n\n const light: AgentTool = {\n name: 'set_global_light',\n title: 'Set the global light',\n description:\n 'Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120°, altitude 30°). One undo step. Returns the light.',\n inputSchema: {\n type: 'object',\n properties: {\n angle: { type: 'number', minimum: -360, maximum: 360, description: \"Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out).\" },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'Degrees above the canvas, for bevels (unchanged when left out).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the global light', ...WRITE_IDEMPOTENT },\n handler: ({ angle, altitude }) => {\n if (angle === undefined && altitude === undefined) throw new LayerOperationError('Give angle, altitude or both.');\n const next: { angle?: number; altitude?: number } = {};\n if (angle !== undefined) next.angle = angle as number;\n if (altitude !== undefined) next.altitude = altitude as number;\n return { globalLight: controller.setGlobalLight(next) };\n },\n };\n\n return [list, add, update, remove, setEnabled, light];\n}\n","/**\n * The paint tools' view model, shared by the React and Svelte editors: the brush options bar's\n * controls, Photoshop's shortcuts (B, Shift+B, E, V, [ ], Shift+[ ], number keys, D, X) and a\n * pointer gesture that turns pointer events into strokes (Brush, Mixer Brush, Eraser) or layer\n * moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type MixerSettings } from '../../paint';\n\nconst plain = { kind: 'plain' } as const;\n\n/** Controls of the brush options bar (`hardness`, `opacity`, `flow`, `smoothing` shown in %). */\nexport const BRUSH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'size' | 'hardness' | 'opacity' | 'flow' | 'smoothing' })[] = [\n { id: 'size', label: 'Size', section: 'layer', min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: 'px' },\n { id: 'hardness', label: 'Hardness', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'flow', label: 'Flow', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'smoothing', label: 'Smoothing', section: 'layer', min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** Controls of the Mixer Brush (shown in %, with the brush controls, while the mixer is active). */\nexport const MIXER_OPTION_CONTROLS: readonly (ControlSpec & { id: 'wet' | 'load' | 'mix' })[] = [\n { id: 'wet', label: 'Wet', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'load', label: 'Load', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'mix', label: 'Mix', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** The Mixer Brush's mixing models, for its RGB / Pigment toggle. */\nexport const MIXER_MIXING_OPTIONS: readonly { id: MixerSettings['mixing']; label: string; title: string }[] = [\n { id: 'rgb', label: 'RGB', title: 'Mix colours as RGB averages' },\n { id: 'pigment', label: 'Pigment', title: 'Mix colours like paint: blue and yellow make green' },\n];\n\n/** The paint tools in the toolbar. */\nexport const PAINT_TOOLS = [\n { id: 'move', label: 'Move', shortcut: 'V' },\n { id: 'brush', label: 'Brush', shortcut: 'B' },\n { id: 'mixer', label: 'Mixer', shortcut: 'Shift+B' },\n { id: 'eraser', label: 'Eraser', shortcut: 'E' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => tool === 'move' || tool === 'brush' || tool === 'mixer' || tool === 'eraser';\n\n/** `#rrggbb` of an sRGB triple (0..1), e.g. the Mixer Brush's reservoir. */\nexport function rgbUnitToHex(c: readonly [number, number, number]): string {\n return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('')}`;\n}\n\n/** Photoshop's bracket step for a brush size. */\nfunction sizeStep(size: number): number {\n return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;\n}\n\nexport interface ShortcutEvent {\n key: string;\n shiftKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n /** The event target: shortcuts are ignored while typing in inputs. */\n target?: unknown;\n}\n\nconst isTyping = (t: unknown) => {\n const el = t as { tagName?: string; isContentEditable?: boolean } | null;\n return Boolean(el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'));\n};\n\n/**\n * Applies a paint shortcut. Returns true when it was one (the caller then prevents the default).\n * B / Shift+B (Mixer Brush) / E / V choose the tool; [ and ] change the size, with Shift the hardness by 25 %; 1..9 set the\n * opacity to 10..90 % and 0 to 100 %; D resets the colours to black and white; X swaps them.\n */\nexport function handlePaintShortcut(controller: ImageEditorController, e: ShortcutEvent): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;\n const brush = controller.getState().paint.brush;\n const set = (b: Partial<BrushSettings>) => controller.setPaintOptions({ brush: b });\n const key = e.key;\n switch (key.toLowerCase()) {\n case 'b':\n controller.setTool(e.shiftKey || key === 'B' ? 'mixer' : 'brush');\n return true;\n case 'e':\n controller.setTool('eraser');\n return true;\n case 'v':\n controller.setTool('move');\n return true;\n case 'd':\n controller.resetPaintColors();\n return true;\n case 'x':\n controller.swapPaintColors();\n return true;\n }\n if (key === '[' || key === '{') {\n if (e.shiftKey || key === '{') set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });\n else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });\n return true;\n }\n if (key === ']' || key === '}') {\n if (e.shiftKey || key === '}') set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });\n else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });\n return true;\n }\n if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {\n set({ opacity: key === '0' ? 1 : Number(key) / 10 });\n return true;\n }\n return false;\n}\n\n/** One pointer sample in document pixels. */\nexport interface PaintPointerSample extends ImagePoint {\n /** 0..1; a mouse is 1. */\n pressure: number;\n}\n\n/** Pressure of a pointer event: pens report it; mice report 0.5 while pressed, which means \"full\". */\nexport function pointerPressure(e: { pointerType?: string; pressure?: number }): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * Turns pointer events into strokes (Brush, Mixer Brush, Eraser on the active layer's target) or layer moves\n * (Move on the active pixel layer). `down` returns an error message when the tool cannot act.\n */\nexport class PaintGesture {\n private stroke: PaintStrokeSession | null = null;\n private move: { id: string; start: ImagePoint; origin: { x: number; y: number }; moved: boolean } | null = null;\n /** The last refusal (e.g. \"layer is locked\"), for the editor to show. */\n error: string | null = null;\n\n constructor(private readonly controller: ImageEditorController) {}\n\n get active(): boolean {\n return Boolean(this.stroke || this.move);\n }\n\n down(sample: PaintPointerSample): string | null {\n this.error = null;\n const tool = this.controller.getState().activeTool;\n try {\n if (tool === 'move') {\n const layer = this.controller.getActiveLayer();\n if (!layer) throw new Error('Select a layer to move in the Layers panel.');\n if (layer.type !== 'pixel') throw new Error(`Only pixel layers move; \"${layer.name}\" is a ${layer.type} layer.`);\n if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer \"${layer.name}\" has its position locked.`);\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };\n return null;\n }\n if (tool === 'brush' || tool === 'eraser' || tool === 'mixer') {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([sample]);\n return null;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.stroke = null;\n this.move = null;\n }\n return this.error;\n }\n\n /** Feeds pointer samples (all coalesced events of one move, in order). */\n moveTo(samples: PaintPointerSample[]): void {\n if (!samples.length) return;\n if (this.stroke) {\n this.stroke.push(samples);\n return;\n }\n const m = this.move;\n if (m) {\n const last = samples[samples.length - 1];\n const x = m.origin.x + Math.round(last.x - m.start.x);\n const y = m.origin.y + Math.round(last.y - m.start.y);\n try {\n // The first move of a drag records the undo step; the rest replace it.\n this.controller.offsetLayer(m.id, { x, y }, !m.moved);\n m.moved = true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.move = null;\n }\n }\n }\n\n up(): void {\n if (this.stroke) {\n try {\n this.stroke.end();\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n }\n this.stroke = null;\n this.move = null;\n }\n\n cancel(): void {\n this.stroke?.cancel();\n this.stroke = null;\n this.move = null;\n }\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { MAX_BRUSH_SIZE, MIXING_MODELS, type BrushSettings, type MixerLoad, type MixerSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\nimport { rgbUnitToHex } from './layers/paint-ui';\nimport { LayerOperationError } from './layers/ops';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to paint (default the active layer; see list_layers).' },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask', 'filter-mask'],\n description: \"pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals.\",\n },\n} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush settings; missing fields use the current paint options (default round, hard, 20 px).',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in px (default 20).' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness 0 (soft) .. 1 (hard, default).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness (default 1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity: overlapping dabs never exceed it (default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (default 1).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Point smoothing 0..0.95 (default 0).' },\n pressureSize: { type: 'boolean', description: 'Point pressure scales the size (default true).' },\n pressureOpacity: { type: 'boolean', description: 'Point pressure scales the opacity (default false).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer (default normal). pigment mixes it in like real paint (blue into yellow gives green) instead of laying it over.' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXER = {\n type: 'object',\n description:\n 'Mixer Brush settings (tool mixer only); missing fields use the current mixer options (wet 0.5, load 0.5, mix 0.5, loadEachStroke true, cleanEachStroke true, mixing rgb).',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: 'How much canvas colour each dab picks up, 0 (dry: paints only the reservoir) .. 1 (default 0.5).' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'How much paint the reservoir holds, 0 .. 1 (default 0.5); below 1 it drains per dab, so long strokes run dry.' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"How far each dab's paint moves from the reservoir towards the picked-up colour, 0 .. 1 (default 0.5).\" },\n loadEachStroke: { type: 'boolean', description: 'Refill the reservoir with `color` (or the foreground) before the stroke (default true). Off, the brush keeps what is left of its last load.' },\n cleanEachStroke: { type: 'boolean', description: 'Empty the pickup well after the stroke (default true). Off, the next stroke starts with the colour this one picked up.' },\n mixing: { type: 'string', enum: [...MIXING_MODELS], description: 'rgb (default): RGB averages. pigment: mixes like paint (Kubelka-Munk), so blue dragged through yellow makes green.' },\n },\n additionalProperties: false,\n} as const;\n\n/** A Mixer Brush load as tool output: hex colours and the amount left. */\nexport function mixerLoadResult(load: MixerLoad): { reservoir: string | null; amount: number; pickup: string | null } {\n return {\n reservoir: load.reservoir ? rgbUnitToHex(load.reservoir) : null,\n amount: Math.round(load.amount * 1000) / 1000,\n pickup: load.pickup ? rgbUnitToHex(load.pickup) : null,\n };\n}\n\nfunction readTarget(args: Record<string, unknown>): { layerId?: string; channel?: LayerTarget } {\n return { layerId: args.layerId as string | undefined, channel: args.channel as LayerTarget | undefined };\n}\n\n/**\n * Agent tools for painting: `paint_stroke` (brush, Mixer Brush or eraser along points, on pixels\n * or a mask), `fill_pixels` and `clear_pixels` (Edit › Fill / Clear inside the selection), each\n * one undo step returning the layer, channel and changed bounds; and the Mixer Brush's load:\n * `get_mixer_brush`, `load_mixer_brush` and `clean_mixer_brush` (view state, no undo).\n */\nexport function createImagePaintTools(controller: ImageEditorController): AgentTool[] {\n const paintStroke: AgentTool = {\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke with the brush, smears and blends paint with the Mixer Brush (tool mixer: its reservoir mixes with colour picked up from the layer under each dab, in RGB or pigment mixing), or removes pixels with the eraser, through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask (the mixer paints pixels only). The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count, and for the mixer its mixerLoad after the stroke ({reservoir, amount, pickup}).',\n inputSchema: {\n type: 'object',\n properties: {\n tool: { type: 'string', enum: ['brush', 'eraser', 'mixer'], description: 'brush (default), eraser, or mixer (the Mixer Brush: picks up and blends the layer colour under each dab).' },\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The path in document pixels; one point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in px.' },\n y: { type: 'number', description: 'Document y in px.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure (default 1).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n color: {\n type: 'string',\n description:\n 'Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals. For the mixer, the colour its reservoir is loaded with before the stroke (given, it loads even with loadEachStroke false).',\n },\n brush: BRUSH,\n mixer: MIXER,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (args) => {\n const tool = args.tool as 'brush' | 'eraser' | 'mixer' | undefined;\n if (args.mixer !== undefined && tool !== 'mixer') throw new LayerOperationError('mixer settings apply to the Mixer Brush only; pass tool \"mixer\", or drop mixer.');\n const result = controller.paintStroke({\n tool,\n points: args.points as StrokePoint[],\n color: args.color as string | undefined,\n brush: args.brush as Partial<BrushSettings> | undefined,\n mixer: args.mixer as Partial<MixerSettings> | undefined,\n ...readTarget(args),\n });\n return result.mixerLoad ? { ...result, mixerLoad: mixerLoadResult(result.mixerLoad) } : result;\n },\n };\n\n const getMixer: AgentTool = {\n name: 'get_mixer_brush',\n title: 'Get the Mixer Brush',\n description:\n \"Reads the Mixer Brush: its settings (wet, load, mix, loadEachStroke, cleanEachStroke, mixing) and what it carries now (reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null). Call it before a paint_stroke with tool mixer and loadEachStroke false, to know the brush's starting paint.\",\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get the Mixer Brush', ...READ },\n handler: () => {\n const { mixer, mixerLoad } = controller.getState().paint;\n return { settings: { ...mixer }, load: mixerLoadResult(mixerLoad) };\n },\n };\n\n const loadMixerTool: AgentTool = {\n name: 'load_mixer_brush',\n title: 'Load the Mixer Brush',\n description:\n 'Fills the Mixer Brush reservoir with a colour (amount 1); the picked-up colour is kept. View state, not an undo step. Use it with loadEachStroke false to carry one load across several paint_stroke calls. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { color: { type: 'string', description: 'Paint colour #rrggbb (default the foreground).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Load the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(controller.loadMixerBrush(args.color as string | undefined)),\n };\n\n const cleanMixerTool: AgentTool = {\n name: 'clean_mixer_brush',\n title: 'Clean the Mixer Brush',\n description:\n 'Cleans the Mixer Brush: empties the pickup well so the next stroke starts without canvas colour; with emptyReservoir also drops the loaded paint (the brush then only smears). View state, not an undo step. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { emptyReservoir: { type: 'boolean', description: 'Also drop the reservoir (default false).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Clean the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(args.emptyReservoir === true ? controller.emptyMixerBrush() : controller.cleanMixerBrush()),\n };\n\n const fill: AgentTool = {\n name: 'fill_pixels',\n title: 'Fill pixels',\n description:\n 'Edit › Fill: fills the selection (or the whole canvas without one, or with useSelection false) on a pixel layer or a mask with a colour (default the foreground) at opacity, with a blend mode. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa (default the foreground).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill opacity 0..1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal). pigment mixes the colour into the pixels like paint, opacity being the share of new paint.' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill pixels', ...WRITE },\n handler: (args) =>\n controller.fillPixels({\n color: args.color as string | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n useSelection: args.useSelection as boolean | undefined,\n ...readTarget(args),\n }),\n };\n\n const clear: AgentTool = {\n name: 'clear_pixels',\n title: 'Clear pixels',\n description:\n 'Edit › Clear: removes the selected pixels of a pixel layer (or, without a selection or with useSelection false, all of them); on a transparency-locked layer or a mask it fills with the background colour instead. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'How much to clear 0..1 (default 1).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Clear pixels', ...DESTRUCTIVE },\n handler: (args) =>\n controller.clearPixels({ opacity: args.opacity as number | undefined, useSelection: args.useSelection as boolean | undefined, ...readTarget(args) }),\n };\n\n return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { describeLayer } from './layer-tools';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { PsdError } from './psd/binary';\nimport { importPsd } from './psd/import';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst DATA = {\n type: 'string',\n minLength: 1,\n description: 'The PSD or PSB file as base64 (a `data:` URL prefix is accepted).',\n} as const;\n\n/** Base64 (or a data URL) → bytes, refusing anything that is not base64 text. */\nexport function psdBytesFromBase64(data: unknown): Uint8Array {\n if (typeof data !== 'string' || !data.trim()) throw new PsdError('data is required: the PSD file as base64.');\n const body = data.trim().replace(/^data:[^,]*;base64,/i, '');\n if (!/^[A-Za-z0-9+/=_\\s-]+$/.test(body)) throw new PsdError('data is not base64; send the PSD file bytes base64-encoded.');\n const bytes = base64ToBytes(body);\n if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);\n return bytes;\n}\n\n/**\n * Agent tools for Photoshop files: `inspect_psd` (what a file holds and how it would map),\n * `import_psd` (open it as the document) and `export_psd` (the document as a PSD).\n */\nexport function createImagePsdTools(controller: ImageEditorController): AgentTool[] {\n const inspect: AgentTool = {\n name: 'inspect_psd',\n title: 'Inspect a PSD',\n description:\n 'Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Inspect a PSD', ...READ },\n handler: ({ data }) => {\n const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });\n return {\n width: res.width,\n height: res.height,\n ...res.info,\n resolution: res.resolution ?? null,\n layers: res.summary.map(({ id: _id, ...s }) => s),\n warnings: res.warnings,\n };\n },\n };\n\n const open: AgentTool = {\n name: 'import_psd',\n title: 'Open a PSD as the document',\n description:\n 'Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); type, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).',\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). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor\\'s composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Export the document as a PSD', ...READ },\n handler: async () => {\n const res = await controller.toPsd();\n return {\n data: bytesToBase64(res.bytes),\n mimeType: 'image/vnd.adobe.photoshop',\n size: res.bytes.length,\n psb: res.psb,\n width: res.width,\n height: res.height,\n warnings: res.warnings,\n };\n },\n };\n\n return [inspect, open, save];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { LayerOperationError } from './layers/ops';\nimport { FREE_TRANSFORM_CONTENTS, type FreeTransformContent, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n TRANSFORM_ANCHORS,\n WARP_STYLES,\n anchorPoint,\n isTransformAnchor,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpStyle,\n} from './transform';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { identityWarpMesh, warpMeshColumns, warpMeshRows } from '../geom/warp';\nimport type { LayerTarget } from './layers/model';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to transform (from list_layers; default the active layer).' },\n content: {\n type: 'string',\n enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== 'selection-outline'),\n description:\n 'What moves: layer (all its pixels with a linked mask), selection (only the selected pixels, committed back into the layer; the outline moves too), selection-copy (Layer via Copy: the selected pixels on a new layer above, then transformed; the original stays) or auto (default: selection when something is selected, else layer).',\n },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask'],\n description: 'pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels).',\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop’s Bicubic).' },\n} as const;\n\nconst GEOMETRY = {\n scaleX: { type: 'number', description: 'Width in percent of the frame (default 100; negative flips horizontally).' },\n scaleY: { type: 'number', description: 'Height in percent of the frame (default 100; negative flips vertically).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise (default 0).' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees: vertical edges lean right for positive angles (default 0).' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees: horizontal edges lean down for positive angles (default 0).' },\n dx: { type: 'number', description: 'Move right after scaling and rotating, in document pixels (default 0).' },\n dy: { type: 'number', description: 'Move down after scaling and rotating, in document pixels (default 0).' },\n reference: {\n description: 'Fixed point of scale, skew and rotate: an anchor of the frame (top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right; default center) or a document point {x, y}.',\n anyOf: [{ type: 'string', enum: TRANSFORM_ANCHORS }, POINT],\n },\n matrix: {\n type: 'array',\n items: { type: 'number' },\n minItems: 6,\n maxItems: 6,\n description: \"Instead of the numeric fields: an affine matrix [a, b, c, d, e, f] in document pixels (x' = a·x + c·y + e, y' = b·x + d·y + f).\",\n },\n corners: {\n type: 'array',\n items: POINT,\n minItems: 4,\n maxItems: 4,\n description:\n 'Instead of the numeric fields: where the frame’s corners go, [top-left, top-right, bottom-right, bottom-left] in document pixels (Distort; for Perspective move a pair of corners symmetrically). The quad must stay convex. Get the frame and its corners from get_transform_frame.',\n },\n} as const;\n\nconst NUMERIC = ['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'dx', 'dy', 'reference'] as const;\n\nfunction geometryFrom(args: Record<string, unknown>): { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams } {\n const numeric = NUMERIC.filter((k) => args[k] !== undefined);\n const ways = [numeric.length > 0, args.matrix !== undefined, args.corners !== undefined].filter(Boolean).length;\n if (ways === 0) throw new LayerOperationError('Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.');\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: numeric fields, matrix or corners, not a mix.');\n if (args.matrix !== undefined) return { matrix: args.matrix as unknown as AffineMatrix };\n if (args.corners !== undefined) return { corners: args.corners as unknown as Quad };\n const params: TransformParams = {};\n for (const k of NUMERIC) if (args[k] !== undefined) (params as Record<string, unknown>)[k] = args[k];\n if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the layer to a line).');\n if (typeof params.reference === 'string' && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference \"${params.reference}\". Use ${TRANSFORM_ANCHORS.join(', ')} or {x, y}.`);\n return { params };\n}\n\nfunction targetFrom(args: Record<string, unknown>): FreeTransformInit {\n const init: FreeTransformInit = {};\n if (typeof args.layerId === 'string') init.layerId = args.layerId;\n if (args.content !== undefined) init.content = args.content as FreeTransformContent;\n init.channel = (args.channel as LayerTarget | undefined) ?? 'pixels';\n if (args.interpolation !== undefined) init.interpolation = args.interpolation as Interpolation;\n return init;\n}\n\nfunction summary(res: FreeTransformResult) {\n return {\n layerId: res.layerId,\n content: res.content,\n moved: res.moved,\n changed: res.changed,\n frame: { ...res.box },\n corners: res.corners.map(roundPoint),\n bounds: res.bounds,\n };\n}\n\n/**\n * Agent tools for Free Transform in the image editor: `get_transform_frame`, `transform_layer`\n * (scale, rotate, skew and move as numbers, a matrix, or distort / perspective corners),\n * `warp_layer` (a preset or a Bézier mesh), `transform_selection` (the outline only),\n * `flip_layer` and `rotate_layer_90`. Every write is one undo step, resamples tile by tile and\n * returns the new bounds.\n */\nexport function createImageTransformTools(controller: ImageEditorController): AgentTool[] {\n const getFrame: AgentTool = {\n name: 'get_transform_frame',\n title: 'Get the Free Transform frame',\n description:\n 'Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. For a smart object (smartObject true) the frame is the contents’ rectangle in contents pixels and the corners are where they land now. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: TARGET.layerId,\n content: { type: 'string', enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET.content.description} selection-outline frames the selection outline alone.` },\n channel: TARGET.channel,\n meshColumns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns of an identity warp mesh to return (omit for none).' },\n meshRows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows of an identity warp mesh to return (default meshColumns).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Get the Free Transform frame', ...READ },\n handler: (args) => {\n const init = targetFrom(args);\n // A copy's frame is the selection's; framing it must not add the layer.\n if (init.content === 'selection-copy') init.content = 'selection';\n const s = controller.beginFreeTransform(init);\n const out: Record<string, unknown> = {\n layerId: s.layerId,\n content: args.content === 'selection-copy' ? 'selection-copy' : s.content,\n moved: [...s.moves],\n frame: { ...s.box },\n corners: s.startGeometry.corners.map(roundPoint),\n center: roundPoint(anchorPoint(s.startGeometry, 'center')),\n };\n if (s.smartObject) out.smartObject = true;\n s.cancel();\n if (args.meshColumns !== undefined) {\n const cols = args.meshColumns as number;\n const rows = (args.meshRows as number | undefined) ?? cols;\n const mesh = identityWarpMesh(s.box, cols, rows);\n out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };\n }\n return out;\n },\n };\n\n const transformLayer: AgentTool = {\n name: 'transform_layer',\n title: 'Free Transform a layer',\n description:\n 'Edit › Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop’s options bar, about a reference point), a matrix, or the frame’s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: { ...TARGET, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Free Transform a layer', ...WRITE },\n handler: (args) => summary(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) })),\n };\n\n const warpLayer: AgentTool = {\n name: 'warp_layer',\n title: 'Warp a layer',\n description:\n 'Edit › Transform › Warp: bends a pixel layer (with its linked mask), the selected pixels, a copy of them, or an unlinked mask through a preset (style with bend and distortions in percent) or a Bézier mesh over the frame (get the frame and an identity mesh to edit from get_transform_frame with meshColumns). Give style or mesh, not both. One undo step. Returns what moved and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n ...TARGET,\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset warp (Photoshop’s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist.' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Preset bend in percent, −100..100 (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Preset horizontal distortion in percent, −100..100 (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Preset vertical distortion in percent, −100..100 (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend along the horizontal (default) or vertical axis.' },\n mesh: {\n type: 'object',\n description: 'A custom warp: patch columns and rows, and the (3·columns + 1) × (3·rows + 1) control points row by row in document pixels (anchors where both indices are multiples of 3, Bézier handles between). Start from get_transform_frame’s identity mesh.',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'Control points, row by row, document pixels.' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Warp a layer', ...WRITE },\n handler: (args) => {\n const hasPreset = ['style', 'bend', 'horizontal', 'vertical', 'orientation'].some((k) => args[k] !== undefined);\n if (hasPreset && args.mesh !== undefined) throw new LayerOperationError('Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.');\n if (!hasPreset && args.mesh === undefined) throw new LayerOperationError('Give a preset style or a mesh.');\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' };\n if (hasPreset) {\n if (args.style === undefined) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(', ')}.`);\n warp = {\n style: args.style as WarpStyle,\n bend: (args.bend as number | undefined) ?? 50,\n horizontal: args.horizontal as number | undefined,\n vertical: args.vertical as number | undefined,\n orientation: args.orientation as 'horizontal' | 'vertical' | undefined,\n };\n } else {\n const m = args.mesh as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n return summary(controller.transformLayer({ ...targetFrom(args), warp }));\n },\n };\n\n const transformSelection: AgentTool = {\n name: 'transform_selection',\n title: 'Transform the selection outline',\n description:\n 'Select › Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection’s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.',\n inputSchema: {\n type: 'object',\n properties: { interpolation: { ...TARGET.interpolation, description: 'Resampling of soft edges: nearest, bilinear or bicubic (default).' }, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Transform the selection outline', ...WRITE },\n handler: (args) => {\n const res = controller.transformLayer({ content: 'selection-outline', interpolation: args.interpolation as Interpolation | undefined, ...geometryFrom(args) });\n return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };\n },\n };\n\n const flipLayer: AgentTool = {\n name: 'flip_layer',\n title: 'Flip a layer',\n description:\n 'Edit › Transform › Flip Horizontal or Vertical: mirrors a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (no resampling). To flip the whole image use flip_image. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left ↔ right, vertical top ↔ bottom.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['axis'],\n additionalProperties: false,\n },\n annotations: { title: 'Flip a layer', ...WRITE },\n handler: (args) => summary(controller.flipLayer(args.axis as 'horizontal' | 'vertical', targetFrom(args))),\n };\n\n const rotate90: AgentTool = {\n name: 'rotate_layer_90',\n title: 'Rotate a layer by quarter turns',\n description:\n 'Edit › Transform › Rotate 90° / 180°: turns a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (pixels land on whole pixels; no resampling). To rotate the whole image use set_rotation. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n turn: { type: 'integer', enum: [90, -90, 180], description: 'Degrees: 90 clockwise, -90 counter-clockwise, or 180.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['turn'],\n additionalProperties: false,\n },\n annotations: { title: 'Rotate a layer by quarter turns', ...WRITE },\n handler: (args) => summary(controller.rotateLayer90(args.turn as 90 | -90 | 180, targetFrom(args))),\n };\n\n return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];\n}\n\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { bytesToBase64 } from '../raster/png';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { WARP_STYLES, type AffineMatrix, type Quad, type WarpMesh, type WarpStyle } from './transform';\nimport { BLEND_MODES } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport { requireSmartObject } from './layers/smart';\nimport { describeLayer, maskSummary } from './layer-tools';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst LAYER_ID = { type: 'string', description: 'Smart object layer id from list_layers (type smart).' } as const;\nconst FILTER_ID = { type: 'string', description: 'Smart filter id from get_smart_object or add_smart_filter.' } as const;\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\nconst FILTER_BLENDS = BLEND_MODES.map((m) => m.id).filter((m) => m !== 'pigment');\n\nconst FILTER_SETTINGS = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it (the eye in the Layers panel; default true).' },\n blend: { type: 'string', enum: FILTER_BLENDS, description: 'How the filter’s result blends with the result below it (Blending Options; default normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of the filter’s result over the result below it, 0..1 (default 1).' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 blue to yellow (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n} as const;\n\nfunction settingsFrom(args: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(FILTER_SETTINGS)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n}\n\n/** A PNG (data URL or base64) as a data URL. */\nfunction imageArg(image: unknown): string {\n if (typeof image !== 'string' || !image.trim()) throw new LayerOperationError('image is required: a PNG as a data URL or base64 (other formats need a browser).');\n return image.trim();\n}\n\n/**\n * Agent tools for smart objects in the image editor: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, and the smart filter stack: `add_smart_filter`,\n * `update_smart_filter`, `remove_smart_filter`, `move_smart_filter`, `set_smart_filters_enabled`,\n * `set_smart_filter_mask`. Every write is one undo step, validates first and returns what changed.\n */\nexport function createImageSmartObjectTools(controller: ImageEditorController): AgentTool[] {\n const smartId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a smart object id from list_layers).');\n requireSmartObject(controller.getLayers(), id);\n return id;\n };\n const info = (id: string) => {\n const i = controller.getSmartObject(id);\n return {\n ...i,\n placement: { ...i.placement, corners: i.placement.corners.map(roundPoint), center: roundPoint(i.placement.center), scaleX: round(i.placement.scaleX), scaleY: round(i.placement.scaleY), rotate: round(i.placement.rotate), skewX: round(i.placement.skewX) },\n };\n };\n const layerSummary = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;\n };\n\n const getSmart: AgentTool = {\n name: 'get_smart_object',\n title: 'Read a smart object',\n description:\n 'Returns a smart object’s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents’ own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a smart object', ...READ },\n handler: ({ layerId }) => info(smartId(layerId)),\n };\n\n const convert: AgentTool = {\n name: 'convert_to_smart_object',\n title: 'Convert to Smart Object',\n description:\n 'Layer › Smart Objects › Convert to Smart Object: sibling layers become one smart object at the topmost one’s position, so later transforms and filters are non-destructive. One pixel layer keeps its id, name, opacity, blend, fill, mask and effects, and its filters become smart filters (it looks exactly the same); several layers, a group or an adjustment become a nested layer document (edit it with edit_smart_object_contents). One undo step. Returns the new layer and its contents.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Sibling layer ids from list_layers (default the active layer).' },\n name: { type: 'string', description: 'Name of the smart object (default the top layer’s name).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Convert to Smart Object', ...WRITE },\n handler: ({ layerIds, name }) => {\n for (const id of (layerIds as string[] | undefined) ?? []) requireLayer(controller.getLayers(), id);\n const id = controller.convertToSmartObject(layerIds as string[] | undefined, { name: name as string | undefined });\n return { layer: layerSummary(id), smartObject: info(id) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_smart_object',\n title: 'Rasterize a smart object',\n description:\n 'Layer › Smart Objects › Rasterize: the smart object becomes a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend and fill). The contents, placement and filters are gone, so later transforms resample the pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a smart object', ...DESTRUCTIVE },\n handler: ({ layerId }) => ({ layer: layerSummary(controller.rasterizeSmartObject(smartId(layerId))) }),\n };\n\n const setTransform: AgentTool = {\n name: 'set_smart_object_transform',\n title: 'Set a smart object’s transform',\n description:\n 'Sets where a smart object’s contents land, absolutely (not relative to the current transform), and renders it again from the contents, so nothing degrades however often it is transformed. Give at most one of: corners (Distort / Perspective), matrix (contents pixels → document), or the numbers scaleX / scaleY (percent of the contents’ own size), rotate, skewX, skewY and x, y (where the contents’ centre goes; unset numbers keep their current values). warp sets a preset or a mesh in contents pixels (null removes it); with nothing else given the corners stay. A linked mask follows a warp-free change. For a relative nudge use transform_layer. One undo step. Returns the placement and bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n corners: { type: 'array', items: POINT, minItems: 4, maxItems: 4, description: 'Where the contents’ corners land: [top-left, top-right, bottom-right, bottom-left] in document pixels. The quad must stay convex.' },\n matrix: { type: 'array', items: { type: 'number' }, minItems: 6, maxItems: 6, description: \"Affine map from contents pixels to document pixels [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\" },\n scaleX: { type: 'number', description: 'Width in percent of the contents’ own width (negative flips).' },\n scaleY: { type: 'number', description: 'Height in percent of the contents’ own height (negative flips).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise.' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees.' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees.' },\n x: { type: 'number', description: 'Document x of the contents’ centre (default where it is now).' },\n y: { type: 'number', description: 'Document y of the contents’ centre (default where it is now).' },\n warp: {\n description: 'A warp over the contents: a preset {style, bend, horizontal, vertical, orientation}, a mesh {columns, rows, points} in contents pixels, or null to remove the warp. Omit to keep the current warp.',\n anyOf: [\n { type: 'null' },\n {\n type: 'object',\n properties: {\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset (Photoshop’s menu).' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Bend in percent (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Horizontal distortion in percent (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Vertical distortion in percent (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend axis (default horizontal).' },\n },\n required: ['style'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'The (3·columns + 1) × (3·rows + 1) control points row by row, in contents pixels (0..width × 0..height is no warp).' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n ],\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest, bilinear or bicubic (default: as before, bicubic for new smart objects).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a smart object’s transform', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' } | null | undefined;\n const w = args.warp as Record<string, unknown> | null | undefined;\n if (w === null) warp = null;\n else if (w && 'style' in w) warp = { style: w.style as WarpStyle, bend: (w.bend as number | undefined) ?? 50, horizontal: w.horizontal as number | undefined, vertical: w.vertical as number | undefined, orientation: w.orientation as 'horizontal' | 'vertical' | undefined };\n else if (w) {\n const m = w as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n const keys = ['corners', 'matrix', 'scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y', 'warp', 'interpolation'];\n if (!keys.some((k) => args[k] !== undefined)) throw new LayerOperationError(`Give something to set: ${keys.join(', ')}.`);\n const res = controller.setSmartObjectTransform(id, {\n corners: args.corners as Quad | undefined,\n matrix: args.matrix as AffineMatrix | undefined,\n scaleX: args.scaleX as number | undefined,\n scaleY: args.scaleY as number | undefined,\n rotate: args.rotate as number | undefined,\n skewX: args.skewX as number | undefined,\n skewY: args.skewY as number | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n warp,\n interpolation: args.interpolation as Interpolation | undefined,\n });\n return { layerId: id, placement: info(id).placement, masksMoved: res.masksMoved, bounds: controller.getLayerBounds(id) };\n },\n };\n\n const replace: AgentTool = {\n name: 'replace_smart_object_contents',\n title: 'Replace a smart object’s contents',\n description:\n 'Layer › Smart Objects › Replace Contents: new raster contents from an image for the smart object and every linked instance (same contents id). Each instance keeps its scale and rotation about the contents’ centre and its smart filters, and renders again. One undo step. Returns the contents id, the new size and the layers that changed.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n image: { type: 'string', description: 'The new contents: a PNG as a data URL or base64 (other formats and URLs need a browser).' },\n name: { type: 'string', description: 'File name of the contents (default the old name with .png).' },\n },\n required: ['layerId', 'image'],\n additionalProperties: false,\n },\n annotations: { title: 'Replace a smart object’s contents', ...DESTRUCTIVE },\n handler: async ({ layerId, image, name }) => controller.replaceSmartObjectContentsFromImage(smartId(layerId), imageArg(image), name as string | undefined),\n };\n\n const exportContents: AgentTool = {\n name: 'export_smart_object_contents',\n title: 'Export a smart object’s contents',\n description:\n 'Layer › Smart Objects › Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG…), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Export a smart object’s contents', ...READ },\n handler: ({ layerId }) => {\n const f = controller.exportSmartObjectContents(smartId(layerId));\n return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };\n },\n };\n\n const copy: AgentTool = {\n name: 'copy_smart_object',\n title: 'Copy a smart object',\n description:\n 'Duplicates a smart object directly above it. linked true (Duplicate Layer) shares the contents, so editing or replacing them changes both; linked false (Layer › Smart Objects › New Smart Object via Copy) gives the copy its own contents. Placement, filters, mask and effects are copied either way. One undo step. Returns the copy.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n linked: { type: 'boolean', description: 'true shares the contents with the original; false (default) makes independent contents.' },\n name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Copy a smart object', ...WRITE },\n handler: ({ layerId, linked, name }) => {\n const id = controller.copySmartObject(smartId(layerId), linked === true, name as string | undefined);\n return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };\n },\n };\n\n const editContents: AgentTool = {\n name: 'edit_smart_object_contents',\n title: 'Edit a smart object’s contents',\n description:\n 'Layer › Smart Objects › Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer \"Layer 0\") with their own undo history. Every other tool (list_layers, paint, adjustments…) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents’ size and layers.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Edit a smart object’s contents', ...WRITE },\n handler: ({ layerId }) => {\n const res = controller.editSmartObjectContents(smartId(layerId));\n const s = controller.getState();\n return { ...res, editing: controller.getEditingContents(), layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))) };\n },\n };\n\n const closeContents: AgentTool = {\n name: 'close_smart_object_contents',\n title: 'Close a smart object’s contents',\n description:\n 'Closes the contents opened with edit_smart_object_contents. save true (default) writes the edits back: the smart object and every linked instance render again, as one undo step in the document; save false drops them. Returns the smart object, whether it changed, the layers that changed and what is still open.',\n inputSchema: {\n type: 'object',\n properties: { save: { type: 'boolean', description: 'true (default) saves the edits into the smart object; false discards them.' } },\n additionalProperties: false,\n },\n annotations: { title: 'Close a smart object’s contents', ...WRITE },\n handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() }),\n };\n\n const addFilter: AgentTool = {\n name: 'add_smart_filter',\n title: 'Add a smart filter',\n description:\n 'Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' },\n index: { type: 'integer', minimum: 0, description: 'Position in the stack, 0 = bottom, applied first (default the top).' },\n ...FILTER_SETTINGS,\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a smart filter', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const filter = controller.addSmartFilter(id, { kind: args.kind as string, ...settingsFrom(args) }, args.index as number | undefined);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_smart_filter',\n title: 'Change a smart filter',\n description:\n 'Changes one smart filter: its settings, enabled, blend or opacity (only the given values; the kind cannot change). A Photoshop filter kept from a PSD (kind psd) only takes enabled, blend and opacity. One undo step. Returns the filter and the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, ...FILTER_SETTINGS },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a smart filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n const changes = settingsFrom(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n const filter = controller.updateSmartFilter(id, args.filterId as string, changes);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_smart_filter',\n title: 'Remove a smart filter',\n description: 'Removes one smart filter (the filter mask goes with the last one). One undo step. Returns the remaining stack.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, filterId: FILTER_ID }, required: ['layerId', 'filterId'], additionalProperties: false },\n annotations: { title: 'Remove a smart filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = smartId(layerId);\n controller.removeSmartFilter(id, filterId as string);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const moveFilter: AgentTool = {\n name: 'move_smart_filter',\n title: 'Reorder a smart filter',\n description: 'Moves one smart filter to a position in the stack (0 = bottom, applied first), as dragging it in the Layers panel. One undo step. Returns the stack in its new order.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, index: { type: 'integer', minimum: 0, description: 'New position, 0 = bottom (applied first).' } },\n required: ['layerId', 'filterId', 'index'],\n additionalProperties: false,\n },\n annotations: { title: 'Reorder a smart filter', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, filterId, index }) => {\n const id = smartId(layerId);\n controller.moveSmartFilter(id, filterId as string, index as number);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_smart_filters_enabled',\n title: 'Turn smart filters on or off',\n description: 'Layer › Smart Filter › Disable / Enable Smart Filters: switches the whole stack; each filter keeps its own enabled flag. One undo step. Returns the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, enabled: { type: 'boolean', description: 'false hides every smart filter; true shows them again.' } },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Turn smart filters on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled }) => {\n const id = smartId(layerId);\n controller.setSmartFiltersEnabled(id, enabled === true);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setMask: AgentTool = {\n name: 'set_smart_filter_mask',\n title: 'Set the smart filter mask',\n description:\n 'Sets the one filter mask of a smart object’s smart filters, which says where the filtered result shows over the unfiltered contents: from reveal-all (white), hide-all (black), selection (the selection’s coverage) or none (remove it); keep changes only its properties. Paint it with paint_stroke channel filter-mask. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection', 'none', 'keep'], description: 'What the mask starts from; none removes it; keep only changes the properties below.' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'How much hidden areas hide, 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Gaussian softening in px (default 0).' },\n enabled: { type: 'boolean', description: 'false keeps the mask but ignores it.' },\n linked: { type: 'boolean', description: 'false keeps the mask in place when the smart object is transformed or moved (default linked).' },\n },\n required: ['layerId', 'from'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the smart filter mask', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['density', 'feather', 'enabled', 'linked']) if (args[k] !== undefined) props[k] = args[k];\n const mask = controller.setSmartFilterMask(id, args.from as 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props);\n return { layerId: id, filterMask: maskSummary(mask ?? undefined) };\n },\n };\n\n return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];\n}\n","import type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind, ImageAdjustments } from './types';\nimport { normalizeProcessVersion } from './color';\nimport {\n ADJUSTMENT_KINDS,\n isAdjustmentKind,\n} from './adjustment-layers';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport {\n ADJUSTMENT_LAYER_CONTROLS,\n ADJUSTMENT_LAYER_OPACITY_CONTROL,\n IMAGE_ADJUSTMENT_CONTROLS,\n clampControlValue,\n findAdjustmentControl,\n findLayerControl,\n readAdjustmentControls,\n setControlPath,\n} from './controls';\nimport { createImageSelectionTools } from './selection-tools';\nimport { createImageCaptureSurface } from './capture';\nimport { createCaptureTool } from '../tools/capture.js';\nimport { createImageEditTools } from './edit-tools';\nimport { createImageLayerTools } from './layer-tools';\nimport { createImageEffectTools } from './effect-tools';\nimport { createImagePaintTools } from './paint-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\nimport { createImageTransformTools } from './transform-tools';\nimport { createImageSmartObjectTools } from './smart-tools';\n\nexport interface ImageEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `image-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n}\n\nconst LAYER_PATH = /^layers\\.([^.]+)\\.(.+)$/;\n\nfunction adjustmentsSummary(adjustments: ImageAdjustments) {\n return {\n process: normalizeProcessVersion(adjustments.process),\n values: readAdjustmentControls(adjustments),\n layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l))),\n };\n}\n\n/**\n * Agent tools for an image editor, for a `ToolRegistry`:\n *\n * - `get_adjustments`: base slider values (by control id), process version and adjustment layers.\n * - `list_controls`: the control table (ids, ranges, defaults, steps), layer kinds and blend modes.\n * - `set_adjustments {values, label}`: sets base controls (`exposure`) and layer parameters\n * (`layers.<id>.vibrance`, `layers.<id>.opacity`) in one undo step; values are clamped to the\n * control ranges and the applied values are echoed.\n * - `add_adjustment_layer {kind, params?, opacity?, blend?, name?}`.\n * - `render_preview {maxSide}`: the rendered image as a PNG, longest side at most `maxSide`.\n * - `capture_view {region, rect?, ...}`: a screenshot of the whole image, the selection or a rect in\n * natural pixels (up to 8× for close inspection), optionally with the selection shaded.\n * - Selections and masks: `get_selection`, `select_region`, `select_by_color`, `modify_selection`,\n * `refine_selection`, `apply_selection_as_mask` (see `createImageSelectionTools`).\n * - Layers, geometry and history: `get_image_info`, `get_histogram`, `update_adjustment_layer`,\n * `remove_adjustment_layer`, `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`,\n * `flip_image`, `undo_image`, `redo_image` (see `createImageEditTools`).\n * - Free Transform: `get_transform_frame`, `transform_layer`, `warp_layer`, `transform_selection`,\n * `flip_layer`, `rotate_layer_90` (see `createImageTransformTools`).\n * - Smart objects and smart filters: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, `add_smart_filter`, `update_smart_filter`, `remove_smart_filter`,\n * `move_smart_filter`, `set_smart_filters_enabled`, `set_smart_filter_mask` (see\n * `createImageSmartObjectTools`).\n */\nexport function createImageEditorToolProvider(\n controller: ImageEditorController,\n options: ImageEditorToolProviderOptions = {}\n): AgentToolProvider {\n const getAdjustments: AgentTool = {\n name: 'get_adjustments',\n title: 'Read image adjustments',\n description:\n 'Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'Read image adjustments',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => adjustmentsSummary(controller.getState().adjustments),\n };\n\n const listControls: AgentTool = {\n name: 'list_controls',\n title: 'List adjustment controls',\n description:\n 'Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'List adjustment controls',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => ({\n controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({\n id, label, section, min, max, step, default: def, ...(requiresProcess ? { requiresProcess } : {}),\n })),\n layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({\n kind,\n label,\n controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({\n id, label: l, min, max, step, default: def,\n })),\n })),\n blendModes: BLEND_MODES.map((m) => m.id),\n layerPathFormat: 'layers.<layerId>.<controlId>',\n }),\n };\n\n const setAdjustments: AgentTool = {\n name: 'set_adjustments',\n title: 'Set image adjustments',\n description:\n 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. \"exposure\"); layer parameters use \"layers.<layerId>.<controlId>\" (e.g. \"layers.adj_1.vibrance\"). Values are clamped to each control range; the applied values are returned.',\n inputSchema: {\n type: 'object',\n properties: {\n values: {\n type: 'object',\n description: 'Map of control id to number. See list_controls for ids and ranges.',\n additionalProperties: { type: 'number' },\n },\n label: { type: 'string', description: 'Short description of the change (echoed back).' },\n },\n required: ['values'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Set image adjustments',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: ({ values, label }) => {\n const entries = Object.entries((values ?? {}) as Record<string, unknown>);\n if (!entries.length) throw new Error('No values given. Pass {\"values\": {\"<control id>\": number}}.');\n const state = controller.getState();\n const base: Partial<ImageAdjustments> = {};\n const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));\n const applied: Record<string, number> = {};\n const clamped: string[] = [];\n const unknown: string[] = [];\n for (const [id, raw] of entries) {\n const m = LAYER_PATH.exec(id);\n if (m) {\n const layer = layers.find((l) => l.id === m[1]);\n const spec = layer ? findLayerControl(layer.kind, m[2]) : undefined;\n if (!layer || !spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n if (spec.id === 'opacity') layer.opacity = v;\n else layer.params = setControlPath(layer.params, spec.id, v) as typeof layer.params;\n applied[id] = v;\n continue;\n }\n const spec = findAdjustmentControl(id);\n if (!spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n (base as Record<string, number>)[id] = v;\n applied[id] = v;\n }\n if (unknown.length) {\n const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(', ');\n throw new Error(\n `Unknown control id(s): ${unknown.join(', ')}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`\n );\n }\n const changedLayers = Object.keys(applied).some((id) => id.startsWith('layers.'));\n controller.setAdjustments(changedLayers ? { ...base, layers } : base);\n const result: Record<string, unknown> = { applied, clamped };\n if (typeof label === 'string' && label) result.label = label;\n const notes: string[] = [];\n if ('tint' in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {\n notes.push('Tint has no effect on process 1 edits.');\n }\n if (notes.length) result.notes = notes;\n return result;\n },\n };\n\n const addLayer: AgentTool = {\n name: 'add_adjustment_layer',\n title: 'Add an adjustment layer',\n description:\n 'Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: ' +\n ADJUSTMENT_KINDS.map((k) => k.kind).join(', ') +\n '. Omitted params are neutral.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Adjustment kind.' },\n params: { type: 'object', description: 'Initial parameters (see list_controls); merged with neutral defaults.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' },\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations.' },\n parentId: { type: 'string', description: 'Put the layer at the top of this group (from list_layers).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Add an adjustment layer',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {\n if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind \"${kind}\".`);\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n if (aboveId !== undefined && parentId !== undefined) throw new Error('Give aboveId or parentId, not both.');\n for (const ref of [aboveId, parentId]) {\n if (ref !== undefined && !controller.getLayer(ref as string)) throw new Error(`Unknown layer \"${ref}\". Call list_layers for ids.`);\n }\n if (parentId !== undefined && controller.getLayer(parentId as string)?.type !== 'group') throw new Error(`Layer \"${parentId}\" is not a group.`);\n const id = controller.addAdjustmentLayer({ kind: kind as AdjustmentKind, params, opacity, blend, name, aboveId, parentId });\n const layer = controller.getAdjustmentLayer(id)!;\n return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };\n },\n };\n\n const renderPreview: AgentTool = {\n name: 'render_preview',\n title: 'Render a preview',\n description:\n 'Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels. With layerId, renders only that layer (a group with its children) through the same crop and rotation.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerId: { type: 'string', description: 'Render only this layer (from list_layers).' },\n },\n additionalProperties: false,\n },\n annotations: {\n title: 'Render a preview',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: async ({ maxSide, layerId }) => {\n if (layerId !== undefined && !controller.getLayer(layerId as string)) throw new Error(`Unknown layer \"${layerId}\". Call list_layers for ids.`);\n const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === 'number' ? maxSide : 512)));\n const state = controller.getState();\n const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };\n const quarter = Math.abs(Math.round((state.transform.rotate % 180) / 90)) % 2 === 1;\n const w = quarter ? crop.height : crop.width;\n const h = quarter ? crop.width : crop.height;\n const k = Math.min(1, side / Math.max(w, h, 1));\n const dataUrl = await controller.toDataURL({\n format: 'image/png',\n width: Math.max(1, Math.round(w * k)),\n height: Math.max(1, Math.round(h * k)),\n ...(layerId !== undefined ? { layerIds: [layerId as string] } : {}),\n });\n const comma = dataUrl.indexOf(',');\n if (!dataUrl.startsWith('data:image/png') || comma < 0 || comma === dataUrl.length - 1) {\n throw new Error('Rendering is not available in this environment.');\n }\n return {\n content: [\n { type: 'image', data: dataUrl.slice(comma + 1), mimeType: 'image/png' },\n { type: 'text', text: `Preview ${Math.round(w * k)}×${Math.round(h * k)}` },\n ],\n };\n },\n };\n\n return {\n id: options.id ?? 'image-editor',\n namespace: options.namespace,\n tools: [\n getAdjustments,\n listControls,\n setAdjustments,\n addLayer,\n renderPreview,\n createCaptureTool(createImageCaptureSurface(controller), { subject: 'the image (source orientation, before crop and rotation)', defaultPadding: 0 }),\n ...createImageSelectionTools(controller),\n ...createImageEditTools(controller),\n ...createImageLayerTools(controller),\n ...createImageTransformTools(controller),\n ...createImageSmartObjectTools(controller),\n ...createImageEffectTools(controller),\n ...createImagePaintTools(controller),\n ...createPigmentTools(),\n ...createImagePsdTools(controller),\n ],\n };\n}\n\n","import type { CurvePoint } from './types';\nimport { curveLutFromLevels } from './adjustment-layers';\n\n/**\n * Framework-neutral helpers for a Curves editor drawn in a `width` × `height` box (levels 0..255\n * on both axes, output up). The React and Svelte curve editors share them.\n */\n\nconst clampLevel = (v: number) => Math.min(255, Math.max(0, Math.round(v)));\n\n/** Box coordinates of a curve point. */\nexport function curvePointToBox(p: CurvePoint, width: number, height: number): { x: number; y: number } {\n return { x: (p.input / 255) * width, y: height - (p.output / 255) * height };\n}\n\n/** The curve point (whole levels, clamped) under box coordinates. */\nexport function boxToCurvePoint(x: number, y: number, width: number, height: number): CurvePoint {\n return { input: clampLevel((x / (width || 1)) * 255), output: clampLevel(((height - y) / (height || 1)) * 255) };\n}\n\n/** An SVG polyline path of the evaluated curve (spline, clamped, constant beyond the end points). */\nexport function curveSvgPath(points: readonly CurvePoint[], width: number, height: number, samples = 64): string {\n const lut = curveLutFromLevels(points);\n const n = lut.length - 1;\n let d = '';\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n const y = lut[Math.round(t * n)];\n d += `${i === 0 ? 'M' : ' L'}${round2(t * width)} ${round2(height - y * height)}`;\n }\n return d;\n}\n\n/** Index of the point within `radius` box units of (x, y), nearest first; -1 for none. */\nexport function hitTestCurvePoint(\n points: readonly CurvePoint[],\n x: number,\n y: number,\n width: number,\n height: number,\n radius = 8\n): number {\n let best = -1;\n let bestD = radius * radius;\n points.forEach((p, i) => {\n const b = curvePointToBox(p, width, height);\n const d = (b.x - x) ** 2 + (b.y - y) ** 2;\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n });\n return best;\n}\n\n/** Inserts a point in input order (replacing one at the same input). Returns the new list and its index. */\nexport function insertCurvePoint(points: readonly CurvePoint[], p: CurvePoint): { points: CurvePoint[]; index: number } {\n const point = { input: clampLevel(p.input), output: clampLevel(p.output) };\n const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));\n let index = out.findIndex((q) => q.input > point.input);\n if (index === -1) index = out.length;\n out.splice(index, 0, point);\n return { points: out, index };\n}\n\n/** Moves point `index`, keeping its input strictly between its neighbours'. */\nexport function moveCurvePoint(points: readonly CurvePoint[], index: number, p: CurvePoint): CurvePoint[] {\n const out = points.map((q) => ({ ...q }));\n if (index < 0 || index >= out.length) return out;\n const lo = index > 0 ? out[index - 1].input + 1 : 0;\n const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;\n out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };\n return out;\n}\n\n/** Removes point `index`; a curve keeps at least two points. */\nexport function removeCurvePoint(points: readonly CurvePoint[], index: number): CurvePoint[] {\n if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));\n return points.filter((_, i) => i !== index).map((q) => ({ ...q }));\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { ImagePoint } from '../types';\nimport type { ImageEditorController } from '../controller';\nimport { ellipsePoints, normalizeRect } from './shapes';\nimport { selectionModeFromModifiers, type SelectionToolOptions } from './tool-options';\n\nexport interface GestureModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Pointer gestures of the selection tools, shared by the React and Svelte image editors: feed\n * it pointer positions in natural image pixels (`canvasToImagePoint(..., false)`).\n *\n * - Magic wand: selects on `begin` (async, as it may read pixels).\n * - Rectangle / ellipse: drag a box; a click without a drag deselects (replace mode).\n * - Lasso: drag a free-hand path, closed on `end`.\n *\n * Shift adds, Alt subtracts, both intersect (overriding the options bar mode). While dragging,\n * {@link draftPath} is the outline to show (`RenderOptions.draftSelectionPath`).\n */\nexport class SelectionGesture {\n private start: ImagePoint | null = null;\n private points: ImagePoint[] = [];\n private current: ImagePoint | null = null;\n private modifiers: GestureModifiers = {};\n /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */\n public error: string | null = null;\n\n constructor(\n readonly controller: ImageEditorController,\n private readonly getOptions: () => SelectionToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The outline of the shape being drawn, or `null`. */\n get draftPath(): ImagePoint[] | null {\n if (!this.start || !this.current) return null;\n const { tool } = this.getOptions();\n if (tool === 'lasso') return this.points.length > 1 ? this.points.slice() : null;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current.x - this.start.x,\n height: this.current.y - this.start.y,\n });\n if (rect.width < 1 && rect.height < 1) return null;\n if (tool === 'ellipse') return ellipsePoints(rect);\n return [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n { x: rect.x, y: rect.y + rect.height },\n ];\n }\n\n /** Pointer down. Resolves once a wand click has been applied. */\n async begin(point: ImagePoint, modifiers: GestureModifiers = {}): Promise<void> {\n const options = this.getOptions();\n this.error = null;\n this.modifiers = modifiers;\n if (options.tool === 'wand') {\n this.start = null;\n try {\n await this.controller.magicWand(point, {\n tolerance: options.tolerance,\n contiguous: options.contiguous,\n antiAlias: options.antiAlias,\n distance: options.distance,\n sampleMerged: options.sampleMerged,\n sample: options.sample,\n mode: selectionModeFromModifiers(modifiers, options.mode),\n });\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n return;\n }\n this.start = point;\n this.current = point;\n this.points = [point];\n }\n\n /** Pointer move; returns true when the draft changed (re-render). */\n move(point: ImagePoint): boolean {\n if (!this.start) return false;\n this.current = point;\n if (this.getOptions().tool === 'lasso') {\n const last = this.points[this.points.length - 1];\n if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;\n this.points.push(point);\n }\n return true;\n }\n\n /** Pointer up: commits the shape. Returns true when the selection changed. */\n end(point?: ImagePoint, modifiers?: GestureModifiers): boolean {\n if (!this.start) return false;\n if (point) this.move(point);\n const options = this.getOptions();\n const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);\n const path = this.draftPath;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current!.x - this.start.x,\n height: this.current!.y - this.start.y,\n });\n this.start = null;\n this.current = null;\n this.points = [];\n if (!path) return mode === 'replace' ? this.controller.deselect() : false;\n const opts = { mode, antiAlias: options.antiAlias };\n if (options.tool === 'lasso') return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;\n return options.tool === 'ellipse' ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);\n }\n\n /** Abandons a drag without changing the selection. */\n cancel(): void {\n this.start = null;\n this.current = null;\n this.points = [];\n }\n}\n","/**\n * Region-based mosaic (pixelation) and box-blur filters for redacting or anonymizing\n * rectangular bounding boxes on an ImageData-like buffer.\n */\n\nexport interface MosaicRegion {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/**\n\t * When true, coordinates are interpreted as normalized values:\n\t * - [0..1] when all values are <= 1\n\t * - [0..1000] when any value is > 1 (e.g. Gemini 0..1000 bounding boxes)\n\t */\n\tnormalized?: boolean;\n}\n\nexport interface MosaicImageDataLike {\n\tdata: Uint8ClampedArray;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Resolves a MosaicRegion to clamped integer pixel bounds [x0, y0, x1, y1] within the image dimensions.\n */\nexport function resolveMosaicRegionBounds(\n\tregion: MosaicRegion,\n\timgWidth: number,\n\timgHeight: number\n): { x0: number; y0: number; x1: number; y1: number } | null {\n\tif (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {\n\t\treturn null;\n\t}\n\n\tlet rx = region.x;\n\tlet ry = region.y;\n\tlet rw = region.width;\n\tlet rh = region.height;\n\n\tconst isZeroToOne =\n\t\trx >= 0 &&\n\t\try >= 0 &&\n\t\trw > 0 &&\n\t\trh > 0 &&\n\t\trx <= 1 &&\n\t\try <= 1 &&\n\t\trw <= 1 &&\n\t\trh <= 1 &&\n\t\t(region.normalized === true ||\n\t\t\t(region.normalized === undefined &&\n\t\t\t\t(imgWidth > 1 || imgHeight > 1) &&\n\t\t\t\t(!Number.isInteger(rx) || !Number.isInteger(ry) || !Number.isInteger(rw) || !Number.isInteger(rh))));\n\n\tif (isZeroToOne) {\n\t\trx = rx * imgWidth;\n\t\try = ry * imgHeight;\n\t\trw = rw * imgWidth;\n\t\trh = rh * imgHeight;\n\t} else if (region.normalized === true) {\n\t\t// 0..1000 normalized coordinates\n\t\trx = (rx / 1000) * imgWidth;\n\t\try = (ry / 1000) * imgHeight;\n\t\trw = (rw / 1000) * imgWidth;\n\t\trh = (rh / 1000) * imgHeight;\n\t}\n\n\tconst x0 = Math.max(0, Math.min(imgWidth, Math.floor(rx)));\n\tconst y0 = Math.max(0, Math.min(imgHeight, Math.floor(ry)));\n\tconst x1 = Math.max(0, Math.min(imgWidth, Math.ceil(rx + rw)));\n\tconst y1 = Math.max(0, Math.min(imgHeight, Math.ceil(ry + rh)));\n\n\tif (x1 <= x0 || y1 <= y0) return null;\n\treturn { x0, y0, x1, y1 };\n}\n\n/**\n * Pixelates each rectangular region by averaging `blockSize x blockSize` blocks.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionMosaic<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tblockSize: number = 12\n): T {\n\tconst { data, width, height } = imageData;\n\tconst step = Math.max(2, Math.round(blockSize));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\n\t\tfor (let by = y0; by < y1; by += step) {\n\t\t\tconst bh = Math.min(step, y1 - by);\n\t\t\tfor (let bx = x0; bx < x1; bx += step) {\n\t\t\t\tconst bw = Math.min(step, x1 - bx);\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tconst count = bw * bh;\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\trSum += data[idx];\n\t\t\t\t\t\tgSum += data[idx + 1];\n\t\t\t\t\t\tbSum += data[idx + 2];\n\t\t\t\t\t\taSum += data[idx + 3];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst rAvg = Math.round(rSum / count);\n\t\t\t\tconst gAvg = Math.round(gSum / count);\n\t\t\t\tconst bAvg = Math.round(bSum / count);\n\t\t\t\tconst aAvg = Math.round(aSum / count);\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\tdata[idx] = rAvg;\n\t\t\t\t\t\tdata[idx + 1] = gAvg;\n\t\t\t\t\t\tdata[idx + 2] = bAvg;\n\t\t\t\t\t\tdata[idx + 3] = aAvg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n\n/**\n * Applies a separable 2-pass box blur to each rectangular region.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionBlur<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tradius: number = 10\n): T {\n\tconst { data, width, height } = imageData;\n\tconst rad = Math.max(1, Math.min(64, Math.round(radius)));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\t\tconst rw = x1 - x0;\n\t\tconst rh = y1 - y0;\n\t\tconst temp = new Uint8ClampedArray(rw * rh * 4);\n\n\t\t// Horizontal pass into temp\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst srcY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, x - rad);\n\t\t\t\tconst kEnd = Math.min(rw - 1, x + rad);\n\t\t\t\tfor (let kx = kStart; kx <= kEnd; kx++) {\n\t\t\t\t\tconst srcIdx = (srcY * width + (x0 + kx)) * 4;\n\t\t\t\t\trSum += data[srcIdx];\n\t\t\t\t\tgSum += data[srcIdx + 1];\n\t\t\t\t\tbSum += data[srcIdx + 2];\n\t\t\t\t\taSum += data[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (y * rw + x) * 4;\n\t\t\t\ttemp[dstIdx] = Math.round(rSum / count);\n\t\t\t\ttemp[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\ttemp[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\ttemp[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\n\t\t// Vertical pass from temp back into imageData.data\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst dstY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, y - rad);\n\t\t\t\tconst kEnd = Math.min(rh - 1, y + rad);\n\t\t\t\tfor (let ky = kStart; ky <= kEnd; ky++) {\n\t\t\t\t\tconst srcIdx = (ky * rw + x) * 4;\n\t\t\t\t\trSum += temp[srcIdx];\n\t\t\t\t\tgSum += temp[srcIdx + 1];\n\t\t\t\t\tbSum += temp[srcIdx + 2];\n\t\t\t\t\taSum += temp[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (dstY * width + (x0 + x)) * 4;\n\t\t\t\tdata[dstIdx] = Math.round(rSum / count);\n\t\t\t\tdata[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\tdata[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\tdata[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n","/**\n * The layers panel's view model, shared by the React and Svelte panels: the rows to show (top of\n * the stack first, groups indented, collapsed groups hiding their children), where a drop lands,\n * and the menu of layer operations with whether each one applies to the active layer.\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport type { ImageEditorController } from '../controller';\nimport { findLayer, type ImageLayer, type SmartFilter, type SmartObjectLayer } from './model';\nimport type { LayerPlacement } from './ops';\n\nexport interface LayerPanelRow {\n layer: ImageLayer;\n /** Nesting depth (0 = top level). */\n depth: number;\n parentId: string | null;\n /** Index among its siblings, 0 = bottom. */\n index: number;\n active: boolean;\n /** Display name (adjustment layers without a name show their kind). */\n label: string;\n /** Clipped to the layer below (drawn with an indent arrow). */\n clipped: boolean;\n}\n\n/** Rows top to bottom; children of collapsed groups are left out. */\nexport function layerPanelRows(layers: readonly ImageLayer[], activeId: string | null): LayerPanelRow[] {\n const rows: LayerPanelRow[] = [];\n const walk = (list: readonly ImageLayer[], depth: number, parentId: string | null) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const layer = list[i];\n rows.push({\n layer,\n depth,\n parentId,\n index: i,\n active: layer.id === activeId,\n label: layerLabel(layer),\n clipped: Boolean(layer.clipped),\n });\n if (layer.type === 'group' && !layer.collapsed) walk(layer.children, depth + 1, layer.id);\n }\n };\n walk(layers, 0, null);\n return rows;\n}\n\nexport function layerLabel(layer: ImageLayer): string {\n if (layer.name) return layer.name;\n if (layer.type === 'adjustment') return adjustmentKindLabel(layer.kind);\n return layer.type === 'group' ? 'Group' : 'Layer';\n}\n\n/** A short type badge for rows without a thumbnail. */\nexport function layerBadge(layer: ImageLayer): string {\n switch (layer.type) {\n case 'pixel':\n return 'px';\n case 'adjustment':\n return 'adj';\n case 'group':\n return 'grp';\n case 'annotation':\n return layer.annotation.type === 'text' ? 'T' : 'ann';\n case 'smart':\n return 'so';\n }\n}\n\n/** Where a row was dropped on: the top or bottom part of the row, or the middle of a group row. */\nexport type DropZone = 'above' | 'below' | 'inside';\n\n/** The zone for a pointer at `fraction` (0 top .. 1 bottom) of a row's height. */\nexport function dropZone(fraction: number, isGroup: boolean): DropZone {\n if (isGroup && fraction > 0.3 && fraction < 0.7) return 'inside';\n return fraction < 0.5 ? 'above' : 'below';\n}\n\n/** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */\nexport function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null {\n if (dragId === targetId) return null;\n const target = findLayer(layers, targetId);\n if (!target) return null;\n if (zone === 'inside' && target.layer.type === 'group') return { parentId: targetId };\n // The panel lists the top first: dropping above a row means higher in the stack.\n return zone === 'above' ? { aboveId: targetId } : { belowId: targetId };\n}\n\nexport type LayerMenuAction =\n | 'new-layer'\n | 'new-group'\n | 'duplicate'\n | 'delete'\n | 'group-selected'\n | 'ungroup'\n | 'merge-down'\n | 'merge-visible'\n | 'flatten'\n | 'via-copy'\n | 'via-cut'\n | 'add-mask'\n | 'add-mask-selection'\n | 'apply-mask'\n | 'delete-mask'\n | 'toggle-mask'\n | 'toggle-mask-link'\n | 'toggle-clip'\n | 'rasterize'\n | 'convert-smart'\n | 'edit-contents'\n | 'smart-via-copy'\n | 'add-filter-mask';\n\nexport interface LayerMenuItem {\n action: LayerMenuAction;\n label: string;\n enabled: boolean;\n}\n\n/** The layer menu for the controller's current state (active layer, selection). */\nexport function layerMenu(controller: ImageEditorController): LayerMenuItem[] {\n const s = controller.getState();\n const active = controller.getActiveLayer();\n const loc = active ? findLayer(s.layers, active.id) : null;\n const siblings = loc ? (loc.parent ? loc.parent.children : s.layers) : [];\n const below = loc ? siblings[loc.index - 1] : undefined;\n const hasSel = controller.hasSelection;\n const isPixel = active?.type === 'pixel';\n return [\n { action: 'new-layer', label: 'New Layer', enabled: true },\n { action: 'new-group', label: 'New Group', enabled: true },\n { action: 'duplicate', label: 'Duplicate Layer', enabled: Boolean(active) },\n { action: 'delete', label: 'Delete Layer', enabled: Boolean(active) },\n { action: 'group-selected', label: 'Group Layer', enabled: Boolean(active) },\n { action: 'ungroup', label: 'Ungroup', enabled: active?.type === 'group' },\n { action: 'merge-down', label: 'Merge Down', enabled: Boolean(active?.visible && below?.type === 'pixel') },\n { action: 'merge-visible', label: 'Merge Visible', enabled: s.layers.some((l) => l.visible) },\n { action: 'flatten', label: 'Flatten Image', enabled: s.layers.length > 0 },\n { action: 'via-copy', label: 'Layer via Copy', enabled: isPixel && hasSel },\n { action: 'via-cut', label: 'Layer via Cut', enabled: isPixel && hasSel },\n { action: 'add-mask', label: 'Add Layer Mask', enabled: Boolean(active) },\n { action: 'add-mask-selection', label: 'Add Mask from Selection', enabled: Boolean(active) && hasSel },\n { action: 'apply-mask', label: 'Apply Layer Mask', enabled: isPixel && Boolean(active?.mask) },\n { action: 'delete-mask', label: 'Delete Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask', label: active?.mask?.enabled === false ? 'Enable Layer Mask' : 'Disable Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask-link', label: active?.mask?.linked === false ? 'Link Layer Mask' : 'Unlink Layer Mask', enabled: Boolean(active?.mask && !active.mask.source) },\n { action: 'rasterize', label: 'Rasterize Layer', enabled: active?.type === 'annotation' || active?.type === 'smart' || (isPixel && Boolean(active?.type === 'pixel' && active.filters?.length)) },\n { action: 'convert-smart', label: 'Convert to Smart Object', enabled: Boolean(active && active.type !== 'annotation') },\n { action: 'edit-contents', label: 'Edit Contents', enabled: active?.type === 'smart' },\n { action: 'smart-via-copy', label: 'New Smart Object via Copy', enabled: active?.type === 'smart' },\n { action: 'add-filter-mask', label: 'Add Smart Filter Mask', enabled: active?.type === 'smart' && !active.smartFilters?.mask },\n { action: 'toggle-clip', label: active?.clipped ? 'Release Clipping Mask' : 'Create Clipping Mask', enabled: Boolean(loc && loc.index > 0) },\n ];\n}\n\n/** Runs a menu action on the active layer. Resolves to the error message when it was refused. */\nexport async function runLayerMenuAction(controller: ImageEditorController, action: LayerMenuAction): Promise<string | null> {\n const active = controller.getActiveLayer();\n const id = active?.id;\n try {\n switch (action) {\n case 'new-layer':\n controller.addPixelLayer();\n break;\n case 'new-group':\n controller.addGroup();\n break;\n case 'duplicate':\n if (id) controller.duplicateLayer(id);\n break;\n case 'delete':\n if (id) controller.removeLayer(id);\n break;\n case 'group-selected':\n if (id) controller.groupLayers([id]);\n break;\n case 'ungroup':\n if (id) controller.ungroupLayer(id);\n break;\n case 'merge-down':\n if (id) await controller.mergeDown(id);\n break;\n case 'merge-visible':\n await controller.mergeVisible();\n break;\n case 'flatten':\n await controller.flattenImage();\n break;\n case 'via-copy':\n case 'via-cut':\n controller.layerViaCopy({ cut: action === 'via-cut' });\n break;\n case 'add-mask':\n if (id) controller.addLayerMask(id, 'reveal-all');\n break;\n case 'add-mask-selection':\n if (id) controller.addLayerMask(id, 'selection');\n break;\n case 'apply-mask':\n if (id) await controller.applyLayerMask(id);\n break;\n case 'delete-mask':\n if (id) controller.removeLayerMask(id);\n break;\n case 'toggle-mask':\n if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });\n break;\n case 'toggle-mask-link':\n if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });\n break;\n case 'rasterize':\n if (id) await controller.rasterizeLayer(id);\n break;\n case 'convert-smart':\n if (id) controller.convertToSmartObject([id]);\n break;\n case 'edit-contents':\n if (id) controller.editSmartObjectContents(id);\n break;\n case 'smart-via-copy':\n if (id) controller.copySmartObject(id, false);\n break;\n case 'add-filter-mask':\n if (id) controller.setSmartFilterMask(id, 'reveal-all');\n break;\n case 'toggle-clip':\n if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });\n break;\n }\n return null;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n}\n\n/** One row of a smart object's filter list (Photoshop shows them under the layer, top filter first). */\nexport interface SmartFilterRow {\n filter: SmartFilter;\n /** Display name: Develop, Blur (radius), or the Photoshop filter's name. */\n label: string;\n /** Index in the stack, 0 = bottom (applied first). */\n index: number;\n /** False for Photoshop filters we keep but do not draw. */\n drawn: boolean;\n}\n\n/** The filter rows of a smart object, top filter first (empty when it has none). */\nexport function smartFilterRows(layer: ImageLayer): SmartFilterRow[] {\n if (layer.type !== 'smart') return [];\n const list = (layer as SmartObjectLayer).smartFilters?.filters ?? [];\n const rows: SmartFilterRow[] = list.map((f, index) => ({\n filter: f,\n index,\n drawn: f.kind !== 'psd',\n label: f.kind === 'psd' ? f.name.replace(/&/g, '').replace(/\\.\\.\\.$/, '') : f.kind === 'blur' ? `Blur (${f.radius} px)` : 'Develop',\n }));\n return rows.reverse();\n}\n\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"]}