@pixerate/editor 0.123.0 → 0.124.1
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.
- package/dist/agent/index.cjs +2 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.js +3 -3
- package/dist/animation/index.d.cts +2 -1
- package/dist/animation/index.d.ts +2 -1
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +11 -733
- package/dist/animation-editor/index.d.ts +11 -733
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/{bezier-C-W9LFyD.d.cts → bezier-CBzKKDjl.d.cts} +2 -66
- package/dist/{bezier-DQZE5dxA.d.ts → bezier-CWv6LQal.d.ts} +2 -66
- package/dist/{brushstrokeRender-M4S65DHT.cjs → brushstrokeRender-EELTV7U4.cjs} +8 -8
- package/dist/{brushstrokeRender-M4S65DHT.cjs.map → brushstrokeRender-EELTV7U4.cjs.map} +1 -1
- package/dist/{brushstrokeRender-CDDRSV2W.js → brushstrokeRender-LRV7GXGD.js} +3 -3
- package/dist/canvas/index.cjs +6 -6
- package/dist/canvas/index.d.cts +10 -6
- package/dist/canvas/index.d.ts +10 -6
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-SMJGE7HM.cjs → chunk-4QQZV5Y5.cjs} +512 -39
- package/dist/chunk-4QQZV5Y5.cjs.map +1 -0
- package/dist/{chunk-TVXFHE4T.js → chunk-64PYNEHJ.js} +45 -12
- package/dist/chunk-64PYNEHJ.js.map +1 -0
- package/dist/{chunk-2IW3BWRT.cjs → chunk-7LQO6OHH.cjs} +2 -2
- package/dist/{chunk-2IW3BWRT.cjs.map → chunk-7LQO6OHH.cjs.map} +1 -1
- package/dist/chunk-CN4PJHUG.cjs +234 -0
- package/dist/chunk-CN4PJHUG.cjs.map +1 -0
- package/dist/chunk-FHYDQOPC.cjs +215 -0
- package/dist/chunk-FHYDQOPC.cjs.map +1 -0
- package/dist/{chunk-KMBC4HNV.js → chunk-J67OLQUQ.js} +2 -2
- package/dist/chunk-JRXJTPES.js +215 -0
- package/dist/chunk-JRXJTPES.js.map +1 -0
- package/dist/{chunk-IPFRWILU.js → chunk-ORHMVUR6.js} +488 -15
- package/dist/chunk-ORHMVUR6.js.map +1 -0
- package/dist/{chunk-6CJRDJCU.cjs → chunk-PEHYWUVV.cjs} +176 -111
- package/dist/chunk-PEHYWUVV.cjs.map +1 -0
- package/dist/chunk-QEG3GRN2.js +234 -0
- package/dist/chunk-QEG3GRN2.js.map +1 -0
- package/dist/{chunk-6ZBR4J62.js → chunk-WATAQ2Z2.js} +79 -14
- package/dist/chunk-WATAQ2Z2.js.map +1 -0
- package/dist/{chunk-RLRUAPBJ.cjs → chunk-Z6OUCS2K.cjs} +47 -14
- package/dist/chunk-Z6OUCS2K.cjs.map +1 -0
- package/dist/effects/index.cjs +605 -0
- package/dist/effects/index.cjs.map +1 -0
- package/dist/effects/index.d.cts +163 -0
- package/dist/effects/index.d.ts +163 -0
- package/dist/effects/index.js +605 -0
- package/dist/effects/index.js.map +1 -0
- package/dist/evaluate-2S3vSPLy.d.cts +55 -0
- package/dist/evaluate-CfmT6xA3.d.ts +55 -0
- package/dist/{export-DfjzHAJI.d.ts → export-BGtXgMJ3.d.ts} +536 -128
- package/dist/{export-Dv0-ZVrV.d.cts → export-BdfY_lH0.d.ts} +392 -5
- package/dist/{export-Dv0-ZVrV.d.ts → export-CbpjwxaI.d.cts} +392 -5
- package/dist/{export-lU5qSpHE.d.cts → export-WmYoWmLM.d.cts} +536 -128
- package/dist/flair-CxB3fOcU.d.ts +739 -0
- package/dist/flair-D7BxyBwL.d.cts +739 -0
- package/dist/gradient-KDfCvhuj.d.cts +66 -0
- package/dist/gradient-KDfCvhuj.d.ts +66 -0
- package/dist/helpers-777mC6dc.d.cts +253 -0
- package/dist/helpers-DX2_7L5t.d.ts +253 -0
- package/dist/image-editor/index.cjs +1296 -805
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +39 -182
- package/dist/image-editor/index.d.ts +39 -182
- package/dist/image-editor/index.js +497 -6
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{opentype-p8Xxf-Wh.d.ts → opentype-14PDI5D2.d.ts} +1 -1
- package/dist/{opentype-BV7wWatY.d.cts → opentype-D80AVmMu.d.cts} +1 -1
- package/dist/paint/index.cjs +2 -2
- package/dist/paint/index.d.cts +32 -10
- package/dist/paint/index.d.ts +32 -10
- package/dist/paint/index.js +1 -1
- package/dist/paint-editor/index.cjs +10 -8
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +11 -5
- package/dist/paint-editor/index.d.ts +11 -5
- package/dist/paint-editor/index.js +4 -2
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/parameters/index.d.cts +4 -184
- package/dist/parameters/index.d.ts +4 -184
- package/dist/{pathfinder-IxcyhO00.d.cts → pathfinder-C0UoFypx.d.cts} +1 -1
- package/dist/{pathfinder-StVChJOT.d.ts → pathfinder-DjISxkUv.d.ts} +1 -1
- package/dist/{player-DrcP2xqj.d.cts → player-CBtEoU2x.d.cts} +2 -54
- package/dist/{player-DV4Jujdq.d.ts → player-cGLrngL_.d.ts} +2 -54
- package/dist/{svg-BtND4TJu.d.cts → svg-BDrjRnL1.d.cts} +3 -2
- package/dist/{svg-BfVXG7hL.d.ts → svg-C-vZ3q50.d.ts} +3 -2
- package/dist/tools/index.cjs +2 -2
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools-C7JtLYzo.d.ts +126 -0
- package/dist/tools-DHKGHiSK.d.cts +126 -0
- package/dist/{types-fuXEme9j.d.ts → types-BcX7sCsn.d.ts} +2 -2
- package/dist/{types-DJF3Ghn5.d.cts → types-CznabG7m.d.cts} +2 -2
- package/dist/types-D3-J3TFf.d.ts +185 -0
- package/dist/types-DMcf_z4J.d.cts +185 -0
- package/dist/vector/index.d.cts +10 -10
- package/dist/vector/index.d.ts +10 -10
- package/dist/video-editor/index.cjs +749 -277
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +265 -138
- package/dist/video-editor/index.d.ts +265 -138
- package/dist/video-editor/index.js +490 -18
- package/dist/video-editor/index.js.map +1 -1
- package/dist/{warp-BjmAgQwq.d.cts → warp-CH9MM435.d.cts} +1 -1
- package/dist/{warp-Ca5fedzS.d.ts → warp-k_PdtXOG.d.ts} +1 -1
- package/package.json +6 -1
- package/skills/pixerate-image-editor/SKILL.md +7 -0
- package/skills/pixerate-image-editor/references/provider-effects.md +35 -0
- package/skills/pixerate-video-editor/SKILL.md +39 -1
- package/dist/chunk-6CJRDJCU.cjs.map +0 -1
- package/dist/chunk-6ZBR4J62.js.map +0 -1
- package/dist/chunk-IPFRWILU.js.map +0 -1
- package/dist/chunk-RLRUAPBJ.cjs.map +0 -1
- package/dist/chunk-SMJGE7HM.cjs.map +0 -1
- package/dist/chunk-TVXFHE4T.js.map +0 -1
- /package/dist/{brushstrokeRender-CDDRSV2W.js.map → brushstrokeRender-LRV7GXGD.js.map} +0 -0
- /package/dist/{chunk-KMBC4HNV.js.map → chunk-J67OLQUQ.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/paint-editor/index.cjs","../../src/paint-editor/document.ts","../../src/paint-editor/dab-stroke.ts","../../src/paint-editor/thick.ts","../../src/paint-editor/stroke.ts","../../src/paint-editor/stencils.ts","../../src/paint-editor/strokes.ts","../../src/paint-editor/render.ts","../../src/paint-editor/serialize.ts","../../src/paint-editor/view.ts","../../src/paint-editor/input.ts","../../src/paint-editor/encode.ts","../../src/paint-editor/wet.ts","../../src/paint-editor/thumbnails.ts","../../src/paint-editor/guides.ts","../../src/paint-editor/color.ts","../../src/paint-editor/mixing-pad.ts","../../src/paint-editor/replay.ts","../../src/paint-editor/controller.ts","../../src/paint-editor/presence.ts","../../src/paint-editor/sticker-tools.ts","../../src/paint-editor/guide-tools.ts","../../src/paint-editor/wet-tools.ts","../../src/paint-editor/thick-tools.ts","../../src/paint-editor/replay-tools.ts","../../src/paint-editor/tools.ts","../../src/paint-editor/ui.ts"],"names":["n","raw","d","HEX","layers","EMPTY","LAYER_ID","COLOR","WRITE","head"],"mappings":"AAAA;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;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;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;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACjMO,IAAM,gBAAA,EAAkB,CAAA;AACxB,IAAM,gBAAA,EAAkB,KAAA;AACxB,IAAM,YAAA,EAAc,GAAA;AAqC3B,IAAI,OAAA,EAAS,CAAA;AAGN,SAAS,eAAA,CAAA,EAA0B;AACxC,EAAA,OAAO,CAAA,EAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,QAAA,CAAS,EAAE,CAAC,CAAA,EAAA;AACrC;AAGoD;AAChB,EAAA;AACpC;AAEO;AACwB,EAAA;AACd,IAAA;AACD,IAAA;AACd,EAAA;AACF;AAGwE;AAC3B,EAAA;AACX,IAAA;AACR,IAAA;AACS,IAAA;AACxB,IAAA;AACT,EAAA;AACoC,EAAA;AACJ,EAAA;AACA,EAAA;AAClC;AAGoC;AACD,EAAA;AACF,EAAA;AACP,EAAA;AAC1B;ADsJsC;AACA;AE3Mf;AAGqD;AAC/C,EAAA;AACG,IAAA;AACK,MAAA;AACI,MAAA;AACpC,IAAA;AACH,EAAA;AACkC,EAAA;AACpC;AAGqD;AACzB,EAAA;AACO,EAAA;AACG,EAAA;AACtC;AAGuF;AAC7D,EAAA;AACA,EAAA;AACE,EAAA;AACN,IAAA;AACuB,IAAA;AACP,IAAA;AACV,IAAA;AAC1B,EAAA;AACoC,EAAA;AACtC;AAGkE;AAC9C,EAAA;AACQ,EAAA;AACU,EAAA;AAED,IAAA;AACF,IAAA;AACH,IAAA;AAC9B,EAAA;AACO,EAAA;AACT;AAGyD;AACrC,EAAA;AACS,EAAA;AACS,IAAA;AACF,IAAA;AAClC,EAAA;AACgC,EAAA;AACV,EAAA;AACa,EAAA;AACrC;AAQ4B;AA2BxB,EAAA;AAFS,IAAA;AACQ,IAAA;AAEe,IAAA;AACF,IAAA;AACL,IAAA;AACM,IAAA;AACA,IAAA;AACG,IAAA;AACP,MAAA;AACE,MAAA;AACF,MAAA;AACzB,IAAA;AAE2B,IAAA;AACE,IAAA;AACE,IAAA;AACJ,IAAA;AACD,IAAA;AACM,IAAA;AACJ,IAAA;AACE,IAAA;AAClC,EAAA;AAtBW,EAAA;AACQ,EAAA;AAzBR,iBAAA;AACM,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACyD,kBAAA;AACzD,EAAA;AAAA;AAEA,EAAA;AACoB,kBAAA;AACK,kBAAA;AACzB,EAAA;AACA,EAAA;AACe,kBAAA;AACA,kBAAA;AACE,kBAAA;AACU,kBAAA;AACT,kBAAA;AACA,mBAAA;AAAA;AA4BN,EAAA;AACK,IAAA;AAClC,EAAA;AAEgD,EAAA;AACjB,IAAA;AACH,IAAA;AACT,MAAA;AACiB,MAAA;AACjB,MAAA;AACe,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AAC9B,MAAA;AACH,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGuB,EAAA;AACQ,IAAA;AACZ,IAAA;AACA,IAAA;AACa,IAAA;AACJ,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AAC9B,IAAA;AACM,IAAA;AACT,EAAA;AAE2E,EAAA;AACnD,IAAA;AACS,MAAA;AACD,MAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAGyF,EAAA;AAC3D,IAAA;AACH,IAAA;AACM,MAAA;AACA,MAAA;AAC/B,IAAA;AACkC,IAAA;AACxB,IAAA;AACuB,IAAA;AACJ,IAAA;AACK,IAAA;AACP,IAAA;AACO,IAAA;AACR,MAAA;AACG,MAAA;AAC7B,IAAA;AACkB,IAAA;AACpB,EAAA;AAE6D,EAAA;AACxC,IAAA;AACO,IAAA;AACC,IAAA;AACK,IAAA;AACA,MAAA;AAC9B,MAAA;AACF,IAAA;AAC+B,IAAA;AACI,MAAA;AACjC,MAAA;AACF,IAAA;AACc,IAAA;AAEe,IAAA;AAElB,IAAA;AACe,IAAA;AACM,MAAA;AACC,MAAA;AACtB,MAAA;AACX,IAAA;AACiC,IAAA;AAC7B,IAAA;AACO,IAAA;AACQ,IAAA;AACY,MAAA;AAChB,MAAA;AACkB,MAAA;AACC,IAAA;AACP,IAAA;AACD,MAAA;AACnB,MAAA;AAC0B,QAAA;AACJ,QAAA;AAC3B,MAAA;AACF,IAAA;AAES,IAAA;AACX,EAAA;AAAA;AAGyE,EAAA;AACxD,IAAA;AACE,IAAA;AACL,IAAA;AACkB,IAAA;AACI,MAAA;AACS,MAAA;AACrB,MAAA;AACX,MAAA;AACX,IAAA;AAC4B,IAAA;AACG,IAAA;AAChB,IAAA;AAEgB,IAAA;AACA,IAAA;AACC,IAAA;AACR,IAAA;AACU,IAAA;AACD,MAAA;AACJ,MAAA;AACC,QAAA;AACI,QAAA;AACX,QAAA;AACI,QAAA;AACA,QAAA;AACzB,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG+E,EAAA;AAC9D,IAAA;AACa,IAAA;AACE,IAAA;AACJ,IAAA;AACK,IAAA;AACA,IAAA;AACC,IAAA;AACR,IAAA;AACU,IAAA;AACD,MAAA;AACJ,MAAA;AACC,QAAA;AACG,QAAA;AAC/B,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG2E,EAAA;AACxD,IAAA;AACW,IAAA;AACK,IAAA;AACF,IAAA;AACG,IAAA;AACL,MAAA;AACR,QAAA;AACY,QAAA;AACA,QAAA;AAC/B,MAAA;AACF,IAAA;AAC6B,IAAA;AACJ,IAAA;AACS,IAAA;AACL,MAAA;AACR,QAAA;AACL,UAAA;AACV,UAAA;AACF,QAAA;AACuB,QAAA;AACK,QAAA;AACF,QAAA;AAC5B,MAAA;AACF,IAAA;AACgC,IAAA;AAClC,EAAA;AACF;AF6KsC;AACA;AGne8B;AAEA;AACb,EAAA;AAC9C,EAAA;AAC4B,IAAA;AACA,IAAA;AACD,IAAA;AAClC,EAAA;AACF;AAUoD;AAChB,EAAA;AACpC;AAGkB;AACF;AAGiD;AAC5B,EAAA;AACE,EAAA;AACH,EAAA;AACpC;AAEsD;AAChB,EAAA;AACtC;AAOsF;AAClD,EAAA;AACA,EAAA;AACD,EAAA;AACP,EAAA;AACU,EAAA;AACjB,EAAA;AACA,EAAA;AACA,EAAA;AACiB,EAAA;AACD,EAAA;AACY,EAAA;AACjB,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACd,IAAA;AACY,IAAA;AAC7B,EAAA;AAC+B,EAAA;AACE,IAAA;AACR,MAAA;AACP,MAAA;AACU,MAAA;AACN,MAAA;AACW,MAAA;AACA,MAAA;AACF,MAAA;AACZ,MAAA;AACA,MAAA;AACgB,MAAA;AACzB,MAAA;AACA,MAAA;AACS,MAAA;AACa,MAAA;AACE,MAAA;AACL,MAAA;AACA,MAAA;AACE,MAAA;AACC,MAAA;AAEE,MAAA;AACF,MAAA;AACD,QAAA;AACK,QAAA;AACH,QAAA;AACC,QAAA;AAC9B,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGsB;AACyD,mBAAA;AAAA;AAGG,EAAA;AACjD,IAAA;AACD,IAAA;AACG,IAAA;AACd,IAAA;AACa,IAAA;AACE,IAAA;AACH,MAAA;AACC,MAAA;AACkB,MAAA;AACpC,MAAA;AACoB,MAAA;AACpB,MAAA;AACoB,MAAA;AACE,MAAA;AACM,MAAA;AACL,MAAA;AACH,MAAA;AACF,MAAA;AACF,QAAA;AACxB,QAAA;AACF,MAAA;AAC8B,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACJ,QAAA;AAChB,MAAA;AACuB,MAAA;AACL,MAAA;AACd,MAAA;AAClB,IAAA;AACiC,IAAA;AACD,IAAA;AAClC,EAAA;AACF;AAG6D;AACvB,EAAA;AACF,EAAA;AACX,EAAA;AACa,EAAA;AAC/B,EAAA;AAC+B,IAAA;AACF,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AA2B+B;AAae,EAAA;AAAvB,IAAA;AACJ,IAAA;AACmB,IAAA;AACP,IAAA;AACb,IAAA;AACgB,IAAA;AACD,IAAA;AACE,MAAA;AACD,MAAA;AACD,MAAA;AAClB,MAAA;AACD,MAAA;AACR,IAAA;AACgC,IAAA;AAEF,IAAA;AAClC,EAAA;AAhBqB,EAAA;AAZV,mBAAA;AACM,EAAA;AACA,EAAA;AACA,EAAA;AAC6C,mBAAA;AACE,mBAAA;AACpB,mBAAA;AACZ,mBAAA;AACG,mBAAA;AACA,mBAAA;AACjB,mBAAA;AAoBD,EAAA;AACA,EAAA;AACyD,mBAAA;AAAA;AAGpD,EAAA;AACW,IAAA;AACjC,EAAA;AAAA;AAG0B,EAAA;AACI,IAAA;AACA,IAAA;AACM,IAAA;AACJ,MAAA;AACX,MAAA;AACF,MAAA;AACjB,IAAA;AAC0B,IAAA;AACC,MAAA;AACR,MAAA;AACF,MAAA;AACjB,IAAA;AACU,IAAA;AACwB,IAAA;AACpC,EAAA;AAEqF,EAAA;AACzD,IAAA;AACC,IAAA;AACpB,IAAA;AAEE,MAAA;AACT,IAAA;AACgC,IAAA;AACC,IAAA;AACA,IAAA;AACX,IAAA;AACf,IAAA;AACT,EAAA;AAEgE,EAAA;AACpC,IAAA;AACG,IAAA;AACrB,IAAA;AAC0B,MAAA;AACjB,MAAA;AACS,MAAA;AAC1B,IAAA;AACO,IAAA;AACT,EAAA;AAE2C,EAAA;AACV,IAAA;AACE,IAAA;AACnC,EAAA;AAEoD,EAAA;AACnB,IAAA;AACG,IAAA;AACpC,EAAA;AAEgD,EAAA;AACjB,IAAA;AACH,IAAA;AACT,MAAA;AACiB,MAAA;AACjB,MAAA;AACe,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AAC9B,MAAA;AACH,IAAA;AACO,IAAA;AACT,EAAA;AAEuB,EAAA;AACQ,IAAA;AACZ,IAAA;AACA,IAAA;AACa,IAAA;AACJ,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AAC9B,IAAA;AACM,IAAA;AACT,EAAA;AAE2E,EAAA;AACnD,IAAA;AACS,MAAA;AACD,MAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AACP,IAAA;AACsB,IAAA;AACxB,EAAA;AAE6D,EAAA;AACvC,IAAA;AACP,IAAA;AACc,IAAA;AACC,IAAA;AACE,IAAA;AACI,IAAA;AACD,IAAA;AACJ,IAAA;AACpB,IAAA;AACsB,MAAA;AACA,QAAA;AACL,UAAA;AACA,UAAA;AACM,UAAA;AAC5B,QAAA;AACJ,IAAA;AAC6B,IAAA;AACK,IAAA;AACA,IAAA;AAEF,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AAEP,IAAA;AAEU,MAAA;AACH,MAAA;AACA,MAAA;AAC9B,MAAA;AACF,IAAA;AAEc,IAAA;AACH,IAAA;AACO,IAAA;AACc,MAAA;AACC,MAAA;AACtB,MAAA;AACX,IAAA;AAC6B,IAAA;AACI,IAAA;AACxB,IAAA;AACwB,IAAA;AACpB,IAAA;AACmB,MAAA;AACE,MAAA;AACD,MAAA;AACjC,IAAA;AAEuB,IAAA;AACW,IAAA;AACL,MAAA;AACV,QAAA;AACH,QAAA;AACQ,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACU,QAAA;AACH,QAAA;AACnB,QAAA;AACmB,QAAA;AACV,QAAA;AACD,UAAA;AAEc,YAAA;AACA,YAAA;AAEH,YAAA;AACvB,YAAA;AACF,UAAA;AACa,UAAA;AACY,YAAA;AACG,YAAA;AAC1B,YAAA;AACF,UAAA;AACc,UAAA;AACW,YAAA;AACG,YAAA;AAC1B,YAAA;AACF,UAAA;AACe,UAAA;AACU,YAAA;AACC,YAAA;AACxB,YAAA;AACF,UAAA;AACF,QAAA;AAC6B,QAAA;AAC/B,MAAA;AACF,IAAA;AACiC,IAAA;AACnC,EAAA;AAAA;AAG8F,EAAA;AAC/D,IAAA;AACd,IAAA;AACI,IAAA;AACW,IAAA;AACI,IAAA;AACD,IAAA;AACF,IAAA;AACG,IAAA;AACD,MAAA;AACV,QAAA;AACP,QAAA;AACK,UAAA;AACf,UAAA;AACF,QAAA;AAE2B,QAAA;AACJ,QAAA;AACO,QAAA;AACpB,QAAA;AACkB,QAAA;AAC9B,MAAA;AACF,IAAA;AAE+B,IAAA;AACG,IAAA;AACrB,IAAA;AACf,EAAA;AAEqF,EAAA;AACtD,IAAA;AACK,IAAA;AACL,MAAA;AACE,QAAA;AACf,QAAA;AACiB,QAAA;AACA,QAAA;AACD,QAAA;AACJ,QAAA;AACE,QAAA;AACG,QAAA;AAClB,QAAA;AACb,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAG+G;AACnG,EAAA;AACsB,EAAA;AACA,IAAA;AACE,IAAA;AAClC,EAAA;AACmC,EAAA;AACrC;AAY0E;AACpD,EAAA;AACL,EAAA;AACkB,EAAA;AACnC;AAG2D;AAC/C,EAAA;AAC0B,EAAA;AACW,EAAA;AACd,IAAA;AACF,IAAA;AACI,IAAA;AACnC,EAAA;AAC+B,EAAA;AACG,EAAA;AACF,EAAA;AACzB,EAAA;AACuB,IAAA;AACE,IAAA;AAChC,EAAA;AACF;AHyYsC;AACA;AIv0ByB;AAC1B,EAAA;AACC,EAAA;AAC7B,EAAA;AACT;AAEgE;AAClC,EAAA;AACR,EAAA;AACQ,EAAA;AACO,EAAA;AAC5B,EAAA;AACT;AAEgE;AACxC,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;AAEgC;AAwB0D,EAAA;AAA3D,IAAA;AAAkC,IAAA;AACzC,IAAA;AACH,IAAA;AACa,IAAA;AACJ,IAAA;AACP,IAAA;AACY,IAAA;AAClB,IAAA;AACY,MAAA;AACX,MAAA;AACK,MAAA;AACE,MAAA;AACF,MAAA;AACM,MAAA;AACM,MAAA;AAChB,MAAA;AACf,IAAA;AACoB,IAAA;AAEK,IAAA;AACC,MAAA;AACH,IAAA;AACwD,MAAA;AACtD,MAAA;AACG,IAAA;AACmD,MAAA;AAC/C,MAAA;AACJ,QAAA;AACH,QAAA;AACD,QAAA;AACT,QAAA;AACC,QAAA;AACK,QAAA;AACN,QAAA;AACE,QAAA;AACQ,QAAA;AACtB,MAAA;AACI,IAAA;AACsB,MAAA;AACC,MAAA;AAC9B,IAAA;AACF,EAAA;AAzC6B,EAAA;AAAkC,EAAA;AAvBtD,EAAA;AAAA;AAEsB,mBAAA;AACd,EAAA;AACA,EAAA;AAC2B,mBAAA;AACF,mBAAA;AACI,mBAAA;AACA,mBAAA;AACO,mBAAA;AACI,mBAAA;AACJ,mBAAA;AAAA;AAE7B,mBAAA;AACP,EAAA;AACA,EAAA;AACmB,mBAAA;AACD,mBAAA;AAClB,mBAAA;AACkB,mBAAA;AAAA;AAE4B,mBAAA;AA6CzB,EAAA;AAC7B,IAAA;AACa,MAAA;AACU,MAAA;AACP,MAAA;AACW,MAAA;AACT,MAAA;AACK,QAAA;AACE,QAAA;AACE,UAAA;AACD,UAAA;AACP,UAAA;AACtB,QAAA;AACkB,QAAA;AACS,QAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AAEoB,EAAA;AACL,IAAA;AACf,EAAA;AAAA;AAGwC,EAAA;AACH,IAAA;AACV,IAAA;AACO,IAAA;AACH,IAAA;AACA,IAAA;AACT,IAAA;AACY,MAAA;AAC9B,MAAA;AACF,IAAA;AACgC,IAAA;AAClC,EAAA;AAAA;AAG6B,EAAA;AACK,IAAA;AAClC,EAAA;AAAA;AAGsC,EAAA;AACF,IAAA;AACpC,EAAA;AAAA;AAGqC,EAAA;AACH,IAAA;AACJ,IAAA;AACG,IAAA;AACjC,EAAA;AAAA;AAG0B,EAAA;AACZ,IAAA;AACd,EAAA;AAAA;AAGuB,EAAA;AACY,IAAA;AACZ,IAAA;AACN,IAAA;AACjB,EAAA;AAE0B,EAAA;AACT,IAAA;AACc,IAAA;AACC,IAAA;AACZ,IAAA;AACO,IAAA;AACQ,IAAA;AACnC,EAAA;AAE0C,EAAA;AACrB,IAAA;AACS,IAAA;AACP,IAAA;AACN,IAAA;AACiB,MAAA;AACb,MAAA;AACc,MAAA;AAC/B,MAAA;AACF,IAAA;AACsB,IAAA;AACN,IAAA;AACO,IAAA;AACS,MAAA;AACE,QAAA;AACH,QAAA;AACE,QAAA;AAC/B,MAAA;AACF,IAAA;AAC8B,IAAA;AACI,MAAA;AACF,MAAA;AACV,MAAA;AACW,MAAA;AACJ,MAAA;AAC7B,IAAA;AAC+B,IAAA;AACjC,EAAA;AAAA;AAGyB,EAAA;AACU,IAAA;AACb,IAAA;AACY,MAAA;AAChB,MAAA;AACgB,MAAA;AACzB,MAAA;AACkB,QAAA;AACS,QAAA;AAChC,MAAA;AACY,MAAA;AACd,IAAA;AACgC,IAAA;AAClB,IAAA;AACO,IAAA;AACW,IAAA;AAClB,IAAA;AACkB,IAAA;AACD,MAAA;AACA,MAAA;AACA,MAAA;AACR,MAAA;AACM,MAAA;AAC7B,IAAA;AAE6B,IAAA;AACC,IAAA;AACG,IAAA;AACH,IAAA;AACI,IAAA;AACT,IAAA;AACO,IAAA;AACpB,IAAA;AACd,EAAA;AAAA;AAGe,EAAA;AACI,IAAA;AACH,IAAA;AACmB,IAAA;AACnC,EAAA;AACF;AAGsD;AACpB,EAAA;AACG,EAAA;AAC5B,EAAA;AACT;AAOsD;AACtB,EAAA;AACE,EAAA;AACG,EAAA;AACL,EAAA;AACvB,EAAA;AACT;AJmzBsC;AACA;AKtnCgC;AAGN;AA+BP;AACuB;AAGhB;AAC1B,EAAA;AACD,EAAA;AAChB,EAAA;AACjB,IAAA;AACA,IAAA;AACkC,IAAA;AACF,IAAA;AACF,IAAA;AACI,IAAA;AACH,IAAA;AACjC,EAAA;AACiC,EAAA;AACA,IAAA;AACJ,IAAA;AACK,IAAA;AACE,IAAA;AACT,EAAA;AACI,IAAA;AACG,IAAA;AACG,IAAA;AACT,IAAA;AACM,IAAA;AAC3B,EAAA;AAC6B,IAAA;AACP,IAAA;AACK,MAAA;AACF,MAAA;AACjB,MAAA;AACN,IAAA;AACuB,MAAA;AACD,MAAA;AACA,MAAA;AAC7B,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGqD;AACvB,EAAA;AACN,EAAA;AACa,IAAA;AACI,IAAA;AACL,IAAA;AACA,IAAA;AACGA,MAAAA;AACF,MAAA;AAChC,IAAA;AACH,EAAA;AACmC,EAAA;AACvB,EAAA;AACsB,EAAA;AACxB,EAAA;AACwB,EAAA;AACF,IAAA;AACG,IAAA;AAClC,EAAA;AACH;AAGoG;AACrE,EAAA;AACA,EAAA;AACX,IAAA;AACI,IAAA;AACD,IAAA;AACA,IAAA;AACe,IAAA;AACH,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AAGsF;AACxD,EAAA;AACd,IAAA;AACW,IAAA;AACA,IAAA;AACK,IAAA;AACK,IAAA;AACC,IAAA;AACA,IAAA;AACpC,EAAA;AACsB,EAAA;AACR,IAAA;AACsB,IAAA;AACV,IAAA;AACd,IAAA;AACsB,MAAA;AACD,MAAA;AAC/B,IAAA;AACyB,IAAA;AAC3B,EAAA;AACyB,EAAA;AAC3B;AAM4B;AAGiC,EAAA;AAA9B,IAAA;AAA+B,EAAA;AAA/B,EAAA;AAFrB,EAAA;AAAA;AAKwB,EAAA;AACT,IAAA;AACvB,EAAA;AAEgC,EAAA;AACA,IAAA;AACiB,MAAA;AACd,MAAA;AACA,QAAA;AACR,QAAA;AACO,QAAA;AAC9B,MAAA;AACwB,MAAA;AAC1B,IAAA;AACwB,IAAA;AACQ,IAAA;AACF,IAAA;AAChC,EAAA;AACF;AAGgE;AAC1D,EAAA;AACwB,EAAA;AAEG,IAAA;AACA,IAAA;AACA,IAAA;AACM,IAAA;AACR,EAAA;AACM,IAAA;AACJ,EAAA;AACjB,IAAA;AACkB,IAAA;AACE,EAAA;AACpB,IAAA;AACS,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AAED,IAAA;AACE,IAAA;AACxB,EAAA;AACS,IAAA;AACD,IAAA;AACoB,IAAA;AAChB,MAAA;AACA,MAAA;AACkB,MAAA;AACnC,IAAA;AACgC,IAAA;AACd,IAAA;AACpB,EAAA;AACuB,EAAA;AACzB;AAMyE;AACxC,EAAA;AACL,EAAA;AACU,EAAA;AACH,EAAA;AACC,IAAA;AACpB,MAAA;AACa,MAAA;AACK,QAAA;AACI,QAAA;AACD,QAAA;AACf,QAAA;AAChB,MAAA;AAC2B,MAAA;AAC7B,IAAA;AACF,EAAA;AAC6B,EAAA;AAC/B;AAG6D;AACd,EAAA;AACtC,EAAA;AACF,IAAA;AACgC,IAAA;AACL,IAAA;AACK,IAAA;AACF,IAAA;AACC,IAAA;AACnB,IAAA;AACM,IAAA;AACvB,EAAA;AACF;ALgkCsC;AACA;AMtzCiB;AAuBgC;AACjD,EAAA;AACtC;AAG4D;AACnD,EAAA;AACmB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACA,IAAA;AACjC,EAAA;AACF;AAEgD;AACV,EAAA;AACH,EAAA;AAChB,EAAA;AACC,EAAA;AACA,EAAA;AACD,EAAA;AACgB,EAAA;AACnC;AAG8D;AAC3B,EAAA;AACH,EAAA;AAChC;AAGkD;AACH,EAAA;AACT,EAAA;AACtC;AAEyD;AAEP;AACX,EAAA;AAC3B,EAAA;AACwB,EAAA;AACG,EAAA;AACH,EAAA;AAC3B,EAAA;AACT;AAEiD;AACd,EAAA;AACnC;AAEgE;AAC7B,EAAA;AACnC;AAEkD;AACX,EAAA;AAC3B,EAAA;AACkB,EAAA;AACG,EAAA;AACG,EAAA;AACN,EAAA;AAC9B;AAG0E;AACnC,EAAA;AAC3B,EAAA;AACsB,EAAA;AACJ,EAAA;AACD,EAAA;AACC,EAAA;AACpB,IAAA;AACK,IAAA;AACmB,IAAA;AACJ,IAAA;AAC1B,IAAA;AACgC,IAAA;AAClC,EAAA;AAC+B,EAAA;AACC,EAAA;AACJ,EAAA;AACP,EAAA;AACU,EAAA;AACN,IAAA;AACoB,IAAA;AACrC,MAAA;AACuBC,QAAAA;AACnB,MAAA;AAER,MAAA;AACF,IAAA;AAC8B,IAAA;AAChC,EAAA;AACgC,EAAA;AACJ,EAAA;AACI,EAAA;AACzB,EAAA;AACT;AAGmH;AACxG,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACe,EAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACvB,EAAA;AACmC,EAAA;AACrC;AAmB8F;AACvE,EAAA;AACa,EAAA;AACR,EAAA;AACA,EAAA;AACA,EAAA;AAEc,EAAA;AACD,EAAA;AACE,EAAA;AACd,EAAA;AAC7B;AAGkG;AAC5D,EAAA;AACN,EAAA;AACI,EAAA;AACN,IAAA;AACM,IAAA;AACzB,IAAA;AACR,EAAA;AACuB,EAAA;AACwB,EAAA;AACf,EAAA;AACN,EAAA;AACC,IAAA;AACM,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AN4vCsC;AACA;AO/5CT;AACE;AAEoE;AACrE,EAAA;AACQ,EAAA;AACtC;AAEuB;AAGoF;AAC7E,EAAA;AACK,EAAA;AACH,EAAA;AACH,IAAA;AACF,IAAA;AACzB,EAAA;AACO,EAAA;AACT;AAEuB;AACD;AAGuC;AACzB,EAAA;AACR,EAAA;AAClB,EAAA;AACyB,IAAA;AACL,IAAA;AACL,IAAA;AACvB,EAAA;AACO,EAAA;AACT;AAE2H;AACrF,EAAA;AACX,EAAA;AACjB,EAAA;AACoB,IAAA;AACA,IAAA;AACN,IAAA;AACtB,EAAA;AACO,EAAA;AACT;AAE6D;AACrC,EAAA;AACQ,IAAA;AACA,IAAA;AACR,MAAA;AACF,MAAA;AACU,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACvB,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACgB,EAAA;AACc,IAAA;AACA,IAAA;AACrB,IAAA;AACT,EAAA;AACF;AAGoF;AACrB,EAAA;AAC/D;AAMuE;AACtC,EAAA;AACA,EAAA;AACE,EAAA;AACC,EAAA;AACf,EAAA;AACC,EAAA;AACS,EAAA;AACZ,IAAA;AACC,IAAA;AACa,IAAA;AACZ,MAAA;AACC,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACsB,MAAA;AACE,QAAA;AACA,QAAA;AACD,UAAA;AACH,UAAA;AACC,UAAA;AACV,UAAA;AACI,UAAA;AACA,UAAA;AACd,UAAA;AACA,UAAA;AACP,QAAA;AACF,MAAA;AACyB,MAAA;AACd,MAAA;AACI,QAAA;AACI,QAAA;AACA,QAAA;AACnB,MAAA;AAC6B,MAAA;AAC/B,IAAA;AACF,EAAA;AACO,EAAA;AACT;APi5CsC;AACA;AQxhDV;AACQ;AA8Bb;AAGmE;AAClD,EAAA;AACF,EAAA;AAC7B,EAAA;AACT;AAgB0H;AACxD,EAAA;AACnC,IAAA;AACnB,IAAA;AACC,IAAA;AACW,IAAA;AACC,IAAA;AACH,IAAA;AACc,IAAA;AACX,IAAA;AACvB,EAAA;AACiC,EAAA;AACK,EAAA;AACF,EAAA;AACE,EAAA;AAC0B,EAAA;AACF,EAAA;AACE,EAAA;AACzD,EAAA;AACT;AAGuC;AACV,EAAA;AACS,EAAA;AACtC;AAGsB;AACa,EAAA;AACG,EAAA;AACtC;AAiB+C;AAClB,EAAA;AACH,EAAA;AACE,IAAA;AACM,MAAA;AACE,MAAA;AACJ,MAAA;AACC,MAAA;AACE,IAAA;AACA,IAAA;AACjC,EAAA;AACO,EAAA;AACT;AAEyE;AACtC,EAAA;AACF,EAAA;AACT,EAAA;AACL,IAAA;AACY,IAAA;AACI,IAAA;AAChC,EAAA;AACM,EAAA;AACT;AAGyC;AACF,EAAA;AAC3B,EAAA;AACqB,EAAA;AACN,EAAA;AACP,IAAA;AAClB,EAAA;AAC4B,EAAA;AACK,EAAA;AACD,EAAA;AACI,EAAA;AACL,EAAA;AACP,EAAA;AACQ,EAAA;AACE,EAAA;AACiB,EAAA;AACvB,EAAA;AACQ,EAAA;AACN,EAAA;AACF,EAAA;AAC9B;AAGsB;AACiB,EAAA;AAC3B,EAAA;AACkB,EAAA;AACP,EAAA;AACN,EAAA;AACmB,EAAA;AACpC;ARm9CsC;AACA;ASznDd;AACA;AAE2C;AAGZ;AACnB,EAAA;AACpB,EAAA;AACI,EAAA;AACE,EAAA;AACU,EAAA;AAChC;AAEyE;AACtC,EAAA;AACH,EAAA;AACvB,EAAA;AACL,IAAA;AACA,IAAA;AACkC,IAAA;AACJ,IAAA;AAChC,EAAA;AACF;AAOuF;AAC/C,EAAA;AACvB,EAAA;AACc,EAAA;AACH,EAAA;AACA,EAAA;AAEA,EAAA;AACC,EAAA;AACK,EAAA;AACC,EAAA;AACjB,EAAA;AACL,EAAA;AACK,EAAA;AACN,EAAA;AACyB,EAAA;AACrC;AAGkD;AAChB,EAAA;AAClC;AAGkD;AACZ,EAAA;AACtC;AAGkD;AACb,EAAA;AACU,EAAA;AACT,EAAA;AACtC;AAGgH;AAC/E,EAAA;AACA,EAAA;AACI,EAAA;AACrC;AAM4G;AACtE,EAAA;AACjB,EAAA;AACmB,EAAA;AACH,EAAA;AACb,IAAA;AACW,IAAA;AACD,IAAA;AAChC,EAAA;AACiC,EAAA;AACR,IAAA;AACD,IAAA;AACU,IAAA;AAClC,EAAA;AACO,EAAA;AACT;ATmmDsC;AACA;AUnrDmB;AAC/B,EAAA;AAC1B;AAO0E;AAClC,EAAA;AACX,EAAA;AACS,EAAA;AACX,EAAA;AACvB,IAAA;AACA,IAAA;AACmB,IAAA;AACD,IAAA;AACT,IAAA;AACA,IAAA;AACV,EAAA;AAEmC,EAAA;AACnB,IAAA;AACD,IAAA;AAChB,EAAA;AACmC,EAAA;AACH,EAAA;AAClC;AAOmG;AAChF,EAAA;AACQ,EAAA;AACA,EAAA;AACO,EAAA;AACC,EAAA;AACnC;AVwqDsC;AACA;AWhuDqB;AAC/C,EAAA;AAIuB,EAAA;AAChB,EAAA;AACF,EAAA;AACc,EAAA;AACK,IAAA;AACE,IAAA;AACR,IAAA;AACI,IAAA;AACJ,IAAA;AACM,IAAA;AACC,IAAA;AACnC,EAAA;AACF;AAMuH;AAC3G,EAAA;AAIyB,EAAA;AACZ,EAAA;AACF,EAAA;AACa,EAAA;AACR,EAAA;AACA,EAAA;AACS,EAAA;AACpB,kBAAA;AACe,EAAA;AAChC;AAG0D;AAC9C,EAAA;AAKsB,EAAA;AACE,EAAA;AACC,EAAA;AAC1B,EAAA;AACI,EAAA;AACL,EAAA;AACgB,EAAA;AAC1B;AXitDsC;AACA;AYjvDjB;AAEc;AAEZ;AAEyC;AACxC,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;AAGmE;AAC7B,EAAA;AACjB,EAAA;AACZ,EAAA;AACa,IAAA;AACE,IAAA;AACQ,IAAA;AACA,IAAA;AACK,IAAA;AACnC,EAAA;AACF;AA8B6B;AAkBe,EAAA;AAArB,IAAA;AACA,IAAA;AACa,IAAA;AACL,IAAA;AACT,IAAA;AACa,IAAA;AAEJ,IAAA;AAEV,IAAA;AACgB,IAAA;AACA,MAAA;AACP,MAAA;AACF,MAAA;AACvB,IAAA;AAEiC,IAAA;AACL,IAAA;AACM,MAAA;AACV,MAAA;AACjB,MAAA;AACN,IAAA;AACY,IAAA;AACmB,IAAA;AAET,IAAA;AACD,IAAA;AACzB,EAAA;AA3BqB,EAAA;AAjBV,mBAAA;AACiB,mBAAA;AAAA;AAEA,mBAAA;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACqB,mBAAA;AAClB,mBAAA;AACD,mBAAA;AACgB,mBAAA;AACA,mBAAA;AACH,mBAAA;AACA,mBAAA;AACY,mBAAA;AAC3B,EAAA;AA+BA,EAAA;AACyD,mBAAA;AAEzD,EAAA;AAAA;AAGY,EAAA;AACK,IAAA;AAClC,EAAA;AAAA;AAGoB,EAAA;AACN,IAAA;AACd,EAAA;AAAA;AAGgD,EAAA;AACjB,IAAA;AACX,IAAA;AACpB,EAAA;AAAA;AAGuB,EAAA;AACJ,IAAA;AACC,IAAA;AACpB,EAAA;AAAA;AAGsB,EAAA;AACS,IAAA;AACI,IAAA;AACP,IAAA;AACK,MAAA;AACG,MAAA;AACjB,MAAA;AACiB,MAAA;AACjB,MAAA;AACe,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AAC9B,MAAA;AACH,IAAA;AACoB,IAAA;AACD,MAAA;AACA,MAAA;AACa,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AACA,QAAA;AAC9B,MAAA;AACe,MAAA;AAClB,IAAA;AAC4B,IAAA;AACrB,IAAA;AACT,EAAA;AAE2E,EAAA;AACnD,IAAA;AACS,MAAA;AACD,MAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAGwF,EAAA;AAC1D,IAAA;AACH,IAAA;AACM,MAAA;AACA,MAAA;AAC/B,IAAA;AACkC,IAAA;AACA,IAAA;AACxB,IAAA;AACuB,IAAA;AACF,IAAA;AACR,IAAA;AACW,IAAA;AACL,MAAA;AACD,QAAA;AACF,UAAA;AACA,UAAA;AACO,UAAA;AAC7B,QAAA;AACa,QAAA;AACY,QAAA;AACI,QAAA;AACL,QAAA;AAC1B,MAAA;AACF,IAAA;AACkB,IAAA;AACpB,EAAA;AAE6D,EAAA;AAC1B,IAAA;AACf,IAAA;AACS,IAAA;AACD,IAAA;AAEO,IAAA;AACnB,IAAA;AACI,IAAA;AACF,MAAA;AACE,QAAA;AACH,QAAA;AACO,QAAA;AAEW,UAAA;AACC,UAAA;AACnB,UAAA;AACX,QAAA;AAC6B,QAAA;AACD,QAAA;AACD,QAAA;AACF,QAAA;AAChB,QAAA;AACgB,QAAA;AACzB,QAAA;AACF,MAAA;AACc,MAAA;AACc,QAAA;AACD,QAAA;AACzB,QAAA;AACF,MAAA;AACK,MAAA;AACwB,QAAA;AAC3B,QAAA;AACW,MAAA;AAEc,QAAA;AACK,QAAA;AAC9B,QAAA;AACF,MAAA;AACK,MAAA;AACuB,QAAA;AAC1B,QAAA;AACa,MAAA;AACW,QAAA;AACxB,QAAA;AACF,MAAA;AACK,MAAA;AACwB,QAAA;AAC3B,QAAA;AACJ,IAAA;AACU,IAAA;AACZ,EAAA;AACF;AAGsF;AACvE,EAAA;AACe,EAAA;AACJ,EAAA;AAC1B;AA8DkE;AAyB1C;AAGO;AAGH;AA0BxB,EAAA;AAFiB,IAAA;AACA,IAAA;AAEiB,IAAA;AACJ,IAAA;AACC,IAAA;AACA,IAAA;AACjC,EAAA;AAPmB,EAAA;AACA,EAAA;AAxBM,mBAAA;AACC,mBAAA;AAC4B,mBAAA;AAC7C,mBAAA;AAAA;AAEI,mBAAA;AACb,EAAA;AACU,mBAAA;AACD,EAAA;AAAA;AAEwB,mBAAA;AACC,mBAAA;AACM,mBAAA;AACpB,mBAAA;AACuB,mBAAA;AAC1B,mBAAA;AACjB,EAAA;AACA,EAAA;AACmC,mBAAA;AACQ,mBAAA;AACC,mBAAA;AAAA;AAAA;AAenB,EAAA;AACO,IAAA;AACA,IAAA;AACX,IAAA;AAEG,MAAA;AACL,MAAA;AACkB,MAAA;AACd,QAAA;AACG,QAAA;AAClB,QAAA;AACF,MAAA;AACW,MAAA;AACmB,MAAA;AACH,MAAA;AAEd,IAAA;AACM,MAAA;AACD,MAAA;AACnB,IAAA;AACL,EAAA;AAAA;AAGyC,EAAA;AACN,IAAA;AACV,IAAA;AACzB,EAAA;AAAA;AAG2D,EAAA;AAC7B,IAAA;AACK,MAAA;AACJ,MAAA;AAC7B,IAAA;AACkC,IAAA;AACnB,IAAA;AACK,IAAA;AACQ,sBAAA;AACR,sBAAA;AACP,MAAA;AACO,MAAA;AACQ,IAAA;AACL,IAAA;AACzB,EAAA;AAE4B,EAAA;AACX,IAAA;AACW,IAAA;AACE,IAAA;AACM,IAAA;AACL,IAAA;AACA,IAAA;AACI,IAAA;AACH,IAAA;AACL,IAAA;AAC3B,EAAA;AAAA;AAGwB,EAAA;AACS,IAAA;AACD,IAAA;AACb,IAAA;AACW,MAAA;AACE,MAAA;AACE,MAAA;AACL,MAAA;AACN,MAAA;AACrB,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAGiC,EAAA;AAChB,IAAA;AACA,IAAA;AACqB,IAAA;AACF,IAAA;AACpC,EAAA;AAAA;AAGwB,EAAA;AACW,IAAA;AACnC,EAAA;AAAA;AAGoB,EAAA;AACa,IAAA;AACjC,EAAA;AAAA;AAG4B,EAAA;AACM,IAAA;AAClC,EAAA;AAEoD,EAAA;AACjB,IAAA;AACzB,IAAA;AACQ,MAAA;AACc,MAAA;AAC9B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG8D,EAAA;AACjC,IAAA;AACF,IAAA;AACA,MAAA;AACG,MAAA;AACM,MAAA;AACD,MAAA;AACD,MAAA;AAChC,IAAA;AACO,IAAA;AACT,EAAA;AAEkE,EAAA;AAC/B,IAAA;AACH,IAAA;AACI,IAAA;AACb,IAAA;AACvB,EAAA;AAEoE,EAAA;AACjC,IAAA;AACA,IAAA;AACC,IAAA;AACD,MAAA;AACH,MAAA;AAC9B,IAAA;AACmB,IAAA;AACrB,EAAA;AAE2C,EAAA;AACP,IAAA;AACL,oBAAA;AACP,IAAA;AACxB,EAAA;AAAA;AAGqB,EAAA;AACE,oBAAA;AACvB,EAAA;AAAA;AAGoC,EAAA;AACD,IAAA;AACmB,IAAA;AAC7C,IAAA;AACL,MAAA;AACsB,MAAA;AACI,MAAA;AACE,MAAA;AACI,MAAA;AACV,MAAA;AACJ,MAAA;AACpB,IAAA;AACF,EAAA;AAEiC,EAAA;AACC,IAAA;AAClC,EAAA;AAAA;AAGgD,EAAA;AAC/B,IAAA;AACC,IAAA;AACC,IAAA;AACY,IAAA;AACC,IAAA;AACI,MAAA;AACH,MAAA;AACN,MAAA;AACzB,IAAA;AAC6B,IAAA;AACtB,IAAA;AACT,EAAA;AAAA;AAG2C,EAAA;AAC1B,IAAA;AACC,IAAA;AACC,IAAA;AACY,IAAA;AACC,IAAA;AACC,MAAA;AACT,MAAA;AACS,MAAA;AACN,MAAA;AACzB,IAAA;AAC6B,IAAA;AACtB,IAAA;AACT,EAAA;AAAA;AAG0B,EAAA;AACT,IAAA;AACP,IAAA;AACsB,IAAA;AACD,MAAA;AACF,MAAA;AACV,MAAA;AACU,MAAA;AAC3B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO+C,EAAA;AAC3B,IAAA;AAEE,MAAA;AACN,MAAA;AACd,IAAA;AAEiC,IAAA;AACf,IAAA;AACF,IAAA;AACD,IAAA;AACmB,IAAA;AACf,MAAA;AACM,MAAA;AACzB,IAAA;AACmB,IAAA;AACA,MAAA;AACF,MAAA;AACQ,MAAA;AACzB,IAAA;AAEkC,IAAA;AAClB,IAAA;AACa,IAAA;AACI,IAAA;AACH,IAAA;AAChB,IAAA;AACH,IAAA;AACQ,MAAA;AACM,MAAA;AACzB,IAAA;AACkB,IAAA;AACO,IAAA;AACnB,MAAA;AACY,QAAA;AACd,MAAA;AACe,QAAA;AACjB,MAAA;AACiB,MAAA;AACA,MAAA;AACM,QAAA;AACH,QAAA;AACd,QAAA;AACI,UAAA;AACN,QAAA;AACkB,UAAA;AACpB,QAAA;AACuB,MAAA;AACV,MAAA;AACG,MAAA;AACjB,IAAA;AACY,IAAA;AACR,IAAA;AACT,EAAA;AAAA;AAGqC,EAAA;AACpB,IAAA;AACmB,IAAA;AACC,IAAA;AAChB,IAAA;AACL,IAAA;AACO,IAAA;AACH,IAAA;AACS,IAAA;AACQ,MAAA;AAClC,IAAA;AACH,EAAA;AAE4B,EAAA;AACX,IAAA;AACQ,IAAA;AACD,IAAA;AACC,IAAA;AACU,IAAA;AACnC,EAAA;AAAA;AAG2D,mBAAA;AAAA;AAGb,EAAA;AAC5B,IAAA;AACD,IAAA;AACA,IAAA;AAEP,IAAA;AACQ,IAAA;AACA,IAAA;AACiB,IAAA;AACR,IAAA;AACnB,MAAA;AACY,QAAA;AACd,MAAA;AACe,QAAA;AACjB,MAAA;AACC,IAAA;AACY,IAAA;AACT,IAAA;AACsB,IAAA;AACL,IAAA;AACR,IAAA;AACE,IAAA;AACnB,EAAA;AAAA;AAAA;AAAA;AAAA;AAMgG,EAAA;AAC/E,IAAA;AACC,IAAA;AACqB,IAAA;AAC+B,IAAA;AACnC,IAAA;AACf,IAAA;AACH,IAAA;AACR,IAAA;AACT,EAAA;AAAA;AAGmD,EAAA;AAClC,IAAA;AACkB,IAAA;AACJ,IAAA;AAC/B,EAAA;AAAA;AAG+B,EAAA;AACd,IAAA;AACgB,IAAA;AACb,IAAA;AACpB,EAAA;AAAA;AAGwC,EAAA;AAC3B,IAAA;AACkB,MAAA;AAC3B,MAAA;AACF,IAAA;AAC+B,IAAA;AACb,IAAA;AACpB,EAAA;AAAA;AAGc,EAAA;AACQ,oBAAA;AACP,IAAA;AACC,IAAA;AACM,IAAA;AACJ,IAAA;AACC,IAAA;AACnB,EAAA;AAAA;AAG2F,EAAA;AAC1E,IAAA;AACC,IAAA;AACyD,IAAA;AAC5C,IAAA;AACA,IAAA;AACI,IAAA;AACA,IAAA;AACH,IAAA;AACE,MAAA;AACW,QAAA;AACT,QAAA;AACH,UAAA;AACjB,UAAA;AACc,UAAA;AACK,UAAA;AAC7B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACK,IAAA;AACT,EAAA;AAEgB,EAAA;AACM,oBAAA;AACP,IAAA;AACK,oBAAA;AACP,IAAA;AACb,EAAA;AACF;AAGkE;AACjC,EAAA;AACE,EAAA;AACnC;AZ6jDsC;AACA;Aap2EJ;AAGa;AACf,EAAA;AACR,oBAAA;AACa,IAAA;AACR,EAAA;AACjB,EAAA;AACgB,EAAA;AACN,EAAA;AACa,EAAA;AACC,EAAA;AAEC,IAAA;AACF,IAAA;AACG,IAAA;AACpC,EAAA;AACmC,EAAA;AAC1B,IAAA;AACC,IAAA;AACR,IAAA;AACiC,IAAA;AACD,IAAA;AACT,IAAA;AACL,IAAA;AACe,IAAA;AAC3B,IAAA;AACgB,IAAA;AACvB,EAAA;AACkC,EAAA;AACrB,EAAA;AAEmB,EAAA;AACP,EAAA;AAC5B;AAU+C;AACpB,EAAA;AACf,EAAA;AACS,EAAA;AACC,EAAA;AACS,EAAA;AACb,IAAA;AACoB,IAAA;AACF,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AAGgD;AACb,EAAA;AACpB,EAAA;AACqB,EAAA;AACd,EAAA;AAClB,IAAA;AACgB,MAAA;AAAC,MAAA;AACM,MAAA;AACL,QAAA;AACa,QAAA;AAC7B,MAAA;AACF,IAAA;AACA,IAAA;AACW,MAAA;AACT,MAAA;AACqB,MAAA;AACH,MAAA;AAClB,MAAA;AACoB,MAAA;AACY,MAAA;AACT,MAAA;AACL,MAAA;AACZ,MAAA;AACI,MAAA;AACK,MAAA;AACO,MAAA;AACQ,MAAA;AACN,MAAA;AACE,MAAA;AAC5B,IAAA;AACF,EAAA;AACoC,EAAA;AACxB,EAAA;AAEsB,EAAA;AACpC;AAEwD;AAClB,EAAA;AACH,EAAA;AAER,EAAA;AACE,EAAA;AACQ,EAAA;AACO,EAAA;AACf,EAAA;AAEQ,IAAA;AACJ,EAAA;AAEP,EAAA;AAEW,IAAA;AACA,IAAA;AACA,IAAA;AACnC,EAAA;AAC+B,EAAA;AACe,EAAA;AACX,EAAA;AACN,EAAA;AAC/B;Abk1EsC;AACA;Ach9EyB;AAkEuB;AAC7E,EAAA;AAC4B,IAAA;AACF,IAAA;AACtB,IAAA;AACI,IAAA;AACF,IAAA;AACb,EAAA;AACF;AAE8B;AAC2B;AACF;AAEsE;AACjH,EAAA;AACc,EAAA;AACjB,EAAA;AAC2B,IAAA;AACH,IAAA;AACI,IAAA;AACF,IAAA;AACH,IAAA;AAC9B,EAAA;AACF;AAGoD;AAC7B,EAAA;AACA,EAAA;AAEK,EAAA;AACL,EAAA;AACK,EAAA;AACS,EAAA;AACrC;AAGmD;AAC3B,EAAA;AACA,EAAA;AACc,EAAA;AACtC;AAE+C;AAGgB;AAC/B,EAAA;AACC,EAAA;AACJ,EAAA;AACO,EAAA;AACC,EAAA;AACrC;AAO2D;AAC7C,EAAA;AACI,EAAA;AACT,IAAA;AACK,MAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AAC0B,MAAA;AAClB,IAAA;AACI,MAAA;AACa,MAAA;AAC9B,IAAA;AACqB,IAAA;AACI,MAAA;AACX,MAAA;AACoB,MAAA;AAClB,MAAA;AACkB,QAAA;AACF,QAAA;AAC9B,MAAA;AACO,MAAA;AACT,IAAA;AACF,EAAA;AACF;AAGuD;AACf,EAAA;AACF,EAAA;AACA,IAAA;AACJ,IAAA;AACG,IAAA;AACxB,IAAA;AACA,IAAA;AACA,IAAA;AACmB,IAAA;AACE,MAAA;AACG,QAAA;AACxB,MAAA;AACsB,QAAA;AACH,QAAA;AACG,QAAA;AACA,QAAA;AAC7B,MAAA;AACF,IAAA;AAC8B,IAAA;AACG,IAAA;AACnC,EAAA;AACO,EAAA;AACT;AAM+F;AACnF,EAAA;AACQ,EAAA;AACW,EAAA;AACI,IAAA;AACC,IAAA;AACb,MAAA;AACa,MAAA;AACD,QAAA;AAC7B,MAAA;AACwB,MAAA;AACzB,IAAA;AACH,EAAA;AACyB,EAAA;AACM,EAAA;AACC,EAAA;AAClC;AAGgF;AACvD,EAAA;AACG,EAAA;AACL,IAAA;AACA,IAAA;AACU,IAAA;AACC,IAAA;AAChC,EAAA;AAEmC,EAAA;AACA,EAAA;AAC5B,EAAA;AACT;AAGoC;AAOJ;AAIwB,EAAA;AAAzB,IAAA;AAA0B,EAAA;AAA1B,EAAA;AAHE,mBAAA;AACF,mBAAA;AAAA;AAK0B,EAAA;AACnB,IAAA;AACpC,EAAA;AAEgC,EAAA;AACG,IAAA;AAChB,IAAA;AACe,MAAA;AACvB,MAAA;AACT,IAAA;AACe,IAAA;AACA,IAAA;AACM,MAAA;AACA,MAAA;AACU,MAAA;AACG,MAAA;AACN,MAAA;AACZ,MAAA;AACE,MAAA;AACe,QAAA;AACV,QAAA;AACP,UAAA;AACHC,UAAAA;AACT,QAAA;AACF,MAAA;AACW,MAAA;AACO,MAAA;AACpB,IAAA;AACe,IAAA;AACkB,IAAA;AACA,IAAA;AACnC,EAAA;AACF;AAG2D;AACnB,EAAA;AACH,EAAA;AACT,EAAA;AAEM,IAAA;AACC,IAAA;AACG,IAAA;AACL,IAAA;AACK,IAAA;AACF,IAAA;AACD,MAAA;AACE,MAAA;AACjC,IAAA;AACF,EAAA;AACO,EAAA;AACT;Ad42EsC;AACA;Ae1oF1B;AAEyC;AACxB,EAAA;AACI,EAAA;AACJ,EAAA;AACS,EAAA;AACtC;AAEuE;AACnC,EAAA;AACpC;AAG+F;AAC/E,EAAA;AACA,EAAA;AACA,EAAA;AACc,EAAA;AACA,EAAA;AACZ,EAAA;AACR,EAAA;AACG,EAAA;AACsB,IAAA;AACA,IAAA;AACF,IAAA;AAC/B,EAAA;AACgB,EAAA;AACmB,EAAA;AACrC;AAEkE;AACzB,EAAA;AACzB,EAAA;AACoB,EAAA;AACpB,EAAA;AACqB,EAAA;AACA,EAAA;AACrC;AAG+F;AAC/D,EAAA;AACE,EAAA;AACA,EAAA;AACR,EAAA;AACR,EAAA;AACkB,EAAA;AACpC;AAEkE;AAC/B,EAAA;AACf,EAAA;AACe,EAAA;AACnC;AAMmD;AAC5C,EAAA;AACE,EAAA;AACC,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACX;AAEmD;AACnB,EAAA;AACF,EAAA;AACE,EAAA;AACH,IAAA;AACJ,IAAA;AACc,IAAA;AACrC,EAAA;AACO,EAAA;AACT;AAGqE;AAChC,EAAA;AACrC;AAGmE;AAChC,EAAA;AACnC;AAIuE;AAaT;AAC7B,EAAA;AACC,EAAA;AACJ,EAAA;AACI,EAAA;AACY,EAAA;AACR,IAAA;AACJ,IAAA;AACI,IAAA;AACpC,EAAA;AACiC,EAAA;AACwC,EAAA;AACjD,IAAA;AACW,IAAA;AACH,IAAA;AACT,IAAA;AACK,IAAA;AACF,IAAA;AAC1B,EAAA;AAC8B,EAAA;AACA,IAAA;AACG,IAAA;AACP,IAAA;AACU,MAAA;AACF,MAAA;AAC/B,IAAA;AACH,EAAA;AACoC,EAAA;AACtC;AAWwD;AACjB,EAAA;AAC3B,EAAA;AACuB,EAAA;AACA,EAAA;AACd,EAAA;AACS,EAAA;AACQ,EAAA;AACJ,EAAA;AACzB,EAAA;AACT;AAGmD;AACvB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACU,EAAA;AACV,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAC5B;Af+lFsC;AACA;AgB7vFN;AACC;AAEiB;AAC5C,EAAA;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACyB,EAAA;AACC,EAAA;AACnC;AAEiD;AAC5C,EAAA;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACI,EAAA;AACoB,EAAA;AAA4G;AAE3G,EAAA;AAClC;AAGgD;AAC3C,EAAA;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACsB,EAAA;AAC/B;AAoBsB;AACJ,mBAAA;AACC,mBAAA;AAC4B,mBAAA;AAAA;AAEpC,mBAAA;AAAA;AAGsK,EAAA;AAChJ,IAAA;AACF,IAAA;AACI,MAAA;AACD,QAAA;AAC7B,MAAA;AACF,IAAA;AAC6B,IAAA;AACG,IAAA;AACD,IAAA;AACjC,EAAA;AAAA;AAGuJ,EAAA;AACpH,IAAA;AACC,IAAA;AACd,IAAA;AACF,MAAA;AAAiC,MAAA;AACjD,MAAA;AACW,QAAA;AACK,QAAA;AACO,QAAA;AACH,QAAA;AAClB,QAAA;AACoB,QAAA;AACQ,QAAA;AACrB,QAAA;AACK,QAAA;AACN,QAAA;AACI,QAAA;AACK,QAAA;AACO,QAAA;AACQ,QAAA;AAChC,MAAA;AACF,IAAA;AACmB,IAAA;AACK,IAAA;AACZ,IAAA;AACK,MAAA;AACV,MAAA;AACP,IAAA;AAC4B,IAAA;AAC9B,EAAA;AAAA;AAGsD,EAAA;AACtB,IAAA;AACG,IAAA;AACC,IAAA;AACA,IAAA;AACF,IAAA;AACC,IAAA;AACN,IAAA;AACnB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACuB,IAAA;AACX,MAAA;AACL,MAAA;AACI,MAAA;AACA,MAAA;AACZ,MAAA;AACP,IAAA;AACoB,IAAA;AACsB,IAAA;AACR,IAAA;AACpC,EAAA;AAEc,EAAA;AACiB,IAAA;AACxB,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY2J,EAAA;AAC5H,IAAA;AACG,IAAA;AACP,IAAA;AACM,MAAA;AAC9B,IAAA;AACiC,IAAA;AACN,IAAA;AACM,IAAA;AACD,IAAA;AACC,IAAA;AACL,IAAA;AACA,IAAA;AACE,IAAA;AACb,MAAA;AAClB,IAAA;AAC+B,IAAA;AACG,IAAA;AAEA,IAAA;AACJ,IAAA;AACP,IAAA;AACU,MAAA;AACD,MAAA;AACE,MAAA;AACD,MAAA;AACA,MAAA;AAClB,MAAA;AACgB,MAAA;AAC9B,IAAA;AAEiC,IAAA;AACD,IAAA;AACL,MAAA;AACZ,MAAA;AACgB,MAAA;AACG,QAAA;AACD,QAAA;AACA,QAAA;AAChC,MAAA;AAC+B,MAAA;AACjC,IAAA;AACc,IAAA;AACkB,MAAA;AACL,MAAA;AAC3B,IAAA;AACiC,IAAA;AACJ,IAAA;AACG,IAAA;AACE,IAAA;AACpC,EAAA;AAAA;AAGgB,EAAA;AACgB,IAAA;AAChC,EAAA;AAAA;AAG2B,EAAA;AACI,IAAA;AACA,IAAA;AACF,IAAA;AACK,IAAA;AACC,IAAA;AAC5B,IAAA;AACP,EAAA;AAAA;AAGqB,EAAA;AACO,IAAA;AAC5B,EAAA;AAAA;AAG4B,EAAA;AACC,IAAA;AACI,IAAA;AACP,MAAA;AACS,MAAA;AACA,MAAA;AACA,MAAA;AACnB,MAAA;AACd,IAAA;AACO,IAAA;AACT,EAAA;AACF;AhB8tFsC;AACA;AiBr4FuB;AACC,EAAA;AAChD,EAAA;AACuB,EAAA;AACX,EAAA;AACA,EAAA;AACY,EAAA;AACD,EAAA;AACrC;AAGoE;AAC9C,EAAA;AACgB,EAAA;AAC7B,EAAA;AACT;AAQE;AAG2B,EAAA;AACa,EAAA;AACR,EAAA;AACL,EAAA;AACO,EAAA;AACpB,EAAA;AAC6B,EAAA;AACP,IAAA;AACf,IAAA;AACF,IAAA;AACe,MAAA;AACJ,MAAA;AAClB,MAAA;AACoB,MAAA;AAC9B,IAAA;AACF,EAAA;AAC6F,EAAA;AACnE,EAAA;AACU,IAAA;AACP,IAAA;AACV,MAAA;AACkB,MAAA;AACH,MAAA;AACD,MAAA;AACG,MAAA;AACsB,QAAA;AAC3B,QAAA;AACG,MAAA;AAC9B,MAAA;AACF,IAAA;AACyB,IAAA;AACR,MAAA;AACM,MAAA;AACE,MAAA;AACnB,MAAA;AACc,QAAA;AACF,UAAA;AACK,YAAA;AACQ,YAAA;AACN,YAAA;AACM,YAAA;AACvB,YAAA;AACF,UAAA;AACK,UAAA;AACoB,YAAA;AACvB,YAAA;AACG,UAAA;AACqB,YAAA;AACxB,YAAA;AACW,UAAA;AAC4B,YAAA;AACd,YAAA;AACF,YAAA;AACC,YAAA;AACxB,YAAA;AACF,UAAA;AACkB,UAAA;AACJ,YAAA;AACY,YAAA;AACxB,YAAA;AACF,UAAA;AACF,QAAA;AACM,MAAA;AAER,MAAA;AACA,MAAA;AACF,IAAA;AAC0B,IAAA;AAC1B,IAAA;AACkC,IAAA;AACP,IAAA;AACM,IAAA;AAEV,MAAA;AACM,MAAA;AACJ,MAAA;AACO,MAAA;AACD,QAAA;AACN,QAAA;AACkB,QAAA;AACzC,MAAA;AACqB,MAAA;AACT,MAAA;AACe,MAAA;AACJ,QAAA;AACE,QAAA;AACO,QAAA;AAChC,MAAA;AACK,IAAA;AACgB,MAAA;AACS,MAAA;AAChC,IAAA;AACgC,IAAA;AAClC,EAAA;AACK,EAAA;AACP;AAG4E;AACvC,EAAA;AACH,EAAA;AAClC;AAGmD;AAClB,EAAA;AACA,EAAA;AACR,EAAA;AACO,EAAA;AACF,EAAA;AACI,EAAA;AACI,IAAA;AACD,IAAA;AACJ,IAAA;AACE,IAAA;AACG,IAAA;AACpC,EAAA;AACgD,EAAA;AAChB,EAAA;AACR,EAAA;AAC1B;AAGiE;AAC7B,EAAA;AACL,EAAA;AACA,EAAA;AACJ,EAAA;AACQ,EAAA;AACH,EAAA;AAClB,EAAA;AACC,EAAA;AACqB,EAAA;AACF,IAAA;AACpB,IAAA;AACC,IAAA;AACsB,IAAA;AACnC,EAAA;AACO,EAAA;AACsB,EAAA;AACF,IAAA;AACR,IAAA;AACiB,MAAA;AACH,sBAAA;AAC/B,IAAA;AACiC,IAAA;AAClC,EAAA;AAEiC,EAAA;AACL,EAAA;AAC/B;AAG4D;AACzC,EAAA;AACiB,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACd,EAAA;AACK,EAAA;AACQ,IAAA;AACJ,IAAA;AACM,IAAA;AACA,IAAA;AAClC,EAAA;AACiC,EAAA;AACpC;AASuE;AACtC,EAAA;AACV,EAAA;AACa,EAAA;AAA+B;AAA2F;AAAO;AACxI,EAAA;AAC7B;AjB42FsC;AACA;AkBljGA;AACP,EAAA;AACd,IAAA;AACD,IAAA;AACd,EAAA;AACF;AA0MY;AAE+B;AACPC,EAAAA;AACF,EAAA;AAClC;AAE+D;AAC9B,EAAA;AACX,EAAA;AACtB;AAE2D;AACvB,EAAA;AACpC;AAEkF;AAClD,EAAA;AACR,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;AAEsF;AAEzE;AACH,EAAA;AACuC,mBAAA;AACV,mBAAA;AACA,mBAAA;AACpB,EAAA;AACT,EAAA;AACA,EAAA;AAAA;AAEwC,mBAAA;AACxC,EAAA;AACiB,mBAAA;AAAA;AAEsB,mBAAA;AAChB,mBAAA;AACwB,mBAAA;AAC/C,EAAA;AACS,EAAA;AACuB,mBAAA;AACtB,mBAAA;AACwB,mBAAA;AACG,mBAAA;AACqB,mBAAA;AACO,mBAAA;AACnC,mBAAA;AAChB,mBAAA;AACE,mBAAA;AAChB,EAAA;AAC2C,mBAAA;AACd,mBAAA;AACE,mBAAA;AACnB,mBAAA;AACK,mBAAA;AACuC,mBAAA;AAC7B,mBAAA;AACT,mBAAA;AACQ,mBAAA;AACE,mBAAA;AAAsB;AAEjD,EAAA;AACQ,EAAA;AAAA;AAEuC,mBAAA;AACD,mBAAA;AAAA;AAEkB,mBAAA;AAAA;AAElB,mBAAA;AAET,EAAA;AACZ,IAAA;AACC,IAAA;AACL,IAAA;AACC,IAAA;AACI,IAAA;AACJ,IAAA;AACA,IAAA;AACF,IAAA;AACM,IAAA;AACP,IAAA;AACX,IAAA;AACb,MAAA;AACyB,QAAA;AACD,QAAA;AACA,QAAA;AACK,UAAA;AACE,UAAA;AAC7B,QAAA;AAC2B,QAAA;AACH,UAAA;AACM,UAAA;AAEF,UAAA;AACD,UAAA;AAC3B,QAAA;AACe,QAAA;AAAC,QAAA;AAClB,MAAA;AACQ,MAAA;AACV,IAAA;AAC8B,IAAA;AAChC,EAAA;AAAA;AAI4C,EAAA;AACf,IAAA;AACC,IAAA;AAC9B,EAAA;AAEwD,EAAA;AAC1B,IAAA;AACC,IAAA;AACxB,IAAA;AACS,IAAA;AACoB,IAAA;AACpC,EAAA;AAAA;AAG6B,EAAA;AACE,IAAA;AACI,IAAA;AACA,IAAA;AACnB,IAAA;AACG,MAAA;AACH,MAAA;AACmB,MAAA;AACnB,MAAA;AACS,MAAA;AACW,MAAA;AAChB,MAAA;AACL,MAAA;AACA,MAAA;AACa,MAAA;AACA,MAAA;AACE,MAAA;AACA,MAAA;AACb,MAAA;AACE,MAAA;AACY,MAAA;AACK,MAAA;AACF,MAAA;AACzB,MAAA;AACgB,QAAA;AACC,QAAA;AACH,QAAA;AACC,QAAA;AACG,QAAA;AACL,QAAA;AACO,QAAA;AACE,QAAA;AACX,QAAA;AACc,QAAA;AAC9B,MAAA;AACc,MAAA;AAChB,IAAA;AACY,IAAA;AACd,EAAA;AAEmC,EAAA;AACrB,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkC,EAAA;AACjB,IAAA;AACD,IAAA;AACP,IAAA;AACT,EAAA;AAAA;AAIwC,EAAA;AACV,IAAA;AAC9B,EAAA;AAEsE,EAAA;AACvB,IAAA;AAC7B,IAAA;AACa,MAAA;AACd,MAAA;AACf,IAAA;AACyB,IAAA;AACQ,IAAA;AACf,IAAA;AACP,IAAA;AACS,IAAA;AACV,IAAA;AACZ,EAAA;AAAA;AAG+B,EAAA;AACZ,IAAA;AACL,IAAA;AAEe,IAAA;AACJ,IAAA;AACK,MAAA;AACA,MAAA;AACX,QAAA;AACC,QAAA;AAChB,MAAA;AACiC,MAAA;AACnC,IAAA;AACF,EAAA;AAAA;AAG2C,EAAA;AACZ,IAAA;AAC/B,EAAA;AAAA;AAG2B,EAAA;AACL,IAAA;AACY,IAAA;AAClC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMyD,EAAA;AACrB,IAAA;AACN,IAAA;AACZ,IAAA;AACkB,IAAA;AACP,IAAA;AACpB,IAAA;AACT,EAAA;AAAA;AAGwC,oBAAA;AAES,EAAA;AAClB,IAAA;AAC/B,EAAA;AAAA;AAG+F,EAAA;AAC1D,IAAA;AACrC,EAAA;AAAA;AAGmE,EAAA;AAC9B,IAAA;AACX,IAAA;AACC,IAAA;AACG,IAAA;AAC9B,EAAA;AAEsB,EAAA;AACM,IAAA;AACI,IAAA;AACf,IAAA;AACgB,IAAA;AACZ,IAAA;AACE,IAAA;AACE,IAAA;AACV,IAAA;AACF,IAAA;AACqB,MAAA;AACA,MAAA;AACA,MAAA;AAChC,IAAA;AACU,IAAA;AACD,IAAA;AACX,EAAA;AAEsB,EAAA;AACM,IAAA;AACI,IAAA;AACf,IAAA;AACgB,IAAA;AACZ,IAAA;AACE,IAAA;AACE,IAAA;AACV,IAAA;AACF,IAAA;AACqB,MAAA;AACA,MAAA;AAChC,IAAA;AACU,IAAA;AACD,IAAA;AACX,EAAA;AAAA;AAGoD,EAAA;AAChB,IAAA;AACpC,EAAA;AAAA;AAIiG,EAAA;AAChE,IAAA;AACF,MAAA;AACT,MAAA;AACX,MAAA;AACT,IAAA;AACiB,IAAA;AACnB,EAAA;AAAA;AAGgH,EAAA;AAC7F,IAAA;AACgB,IAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACJ,MAAA;AAC1B,IAAA;AACkB,IAAA;AACa,IAAA;AAC3B,IAAA;AACe,IAAA;AACF,MAAA;AACD,MAAA;AACjB,IAAA;AAC8B,IAAA;AACvB,IAAA;AACT,EAAA;AAAA;AAGqD,EAAA;AAClC,IAAA;AACc,IAAA;AACG,MAAA;AACF,MAAA;AACF,MAAA;AAC7B,IAAA;AAC6B,IAAA;AAEC,MAAA;AACd,MAAA;AACjB,IAAA;AACgC,IAAA;AACzB,IAAA;AACT,EAAA;AAAA;AAG8E,EAAA;AAC3D,IAAA;AACG,IAAA;AACe,IAAA;AACJ,MAAA;AACX,MAAA;AACX,MAAA;AACT,IAAA;AACgC,IAAA;AACF,IAAA;AACI,IAAA;AACF,IAAA;AACC,IAAA;AACR,IAAA;AACM,IAAA;AACT,IAAA;AACC,IAAA;AACzB,EAAA;AAAA;AAI6C,EAAA;AACT,IAAA;AACpC,EAAA;AAAA;AAGyC,EAAA;AACP,IAAA;AAClC,EAAA;AAAA;AAG+B,EAAA;AACE,IAAA;AACqC,IAAA;AACpC,MAAA;AACZ,QAAA;AACY,QAAA;AACC,QAAA;AAC/B,MAAA;AACF,IAAA;AAC6B,IAAA;AACtB,IAAA;AACT,EAAA;AAEuD,EAAA;AACrB,IAAA;AACR,IAAA;AACI,MAAA;AACM,MAAA;AACX,IAAA;AAChB,IAAA;AACC,MAAA;AACE,MAAA;AACA,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACiB,MAAA;AACE,MAAA;AACF,MAAA;AAC1B,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAE6C,EAAA;AACjB,IAAA;AACR,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAGgF,EAAA;AAC/C,IAAA;AACZ,IAAA;AACW,IAAA;AACE,IAAA;AACA,IAAA;AACN,IAAA;AACnB,IAAA;AACT,EAAA;AAEiC,EAAA;AACX,IAAA;AACe,IAAA;AAET,IAAA;AAChB,IAAA;AACZ,EAAA;AAAA;AAGgI,EAAA;AAC7F,IAAA;AACA,IAAA;AACJ,IAAA;AACG,IAAA;AAEX,IAAA;AACS,IAAA;AAC1B,IAAA;AAC8B,IAAA;AACT,IAAA;AACK,MAAA;AACE,MAAA;AACjB,MAAA;AACG,IAAA;AACe,MAAA;AACX,MAAA;AACS,MAAA;AACT,MAAA;AACtB,IAAA;AACgC,IAAA;AACD,IAAA;AACA,IAAA;AACjC,EAAA;AAE6D,EAAA;AAC1C,IAAA;AACa,IAAA;AACC,IAAA;AACK,IAAA;AACD,IAAA;AACN,MAAA;AAC7B,IAAA;AACkC,IAAA;AACd,IAAA;AACgB,IAAA;AACF,IAAA;AACA,IAAA;AAC0B,IAAA;AAC1B,IAAA;AACF,IAAA;AACE,MAAA;AACA,MAAA;AAClC,IAAA;AACkC,IAAA;AACJ,IAAA;AAChC,EAAA;AAE2E,EAAA;AACxD,IAAA;AACa,IAAA;AACD,IAAA;AACL,IAAA;AACL,IAAA;AACD,MAAA;AACa,MAAA;AAC/B,IAAA;AACiC,IAAA;AACX,IAAA;AACW,IAAA;AACA,IAAA;AACX,IAAA;AACxB,EAAA;AAE2C,EAAA;AACxB,IAAA;AACgB,IAAA;AACgB,MAAA;AACpB,MAAA;AAC7B,IAAA;AAC+B,IAAA;AACX,IAAA;AACwC,IAAA;AACD,IAAA;AACf,IAAA;AACf,MAAA;AACE,MAAA;AACE,MAAA;AACD,MAAA;AAChC,IAAA;AACkC,IAAA;AACF,IAAA;AACD,IAAA;AACF,IAAA;AAC/B,EAAA;AAAA;AAG+E,EAAA;AAC5C,IAAA;AACF,IAAA;AACX,IAAA;AACQ,IAAA;AAChB,IAAA;AACkB,MAAA;AACE,MAAA;AACE,MAAA;AAClC,IAAA;AACkC,IAAA;AACP,IAAA;AACK,IAAA;AACL,IAAA;AACI,IAAA;AACD,IAAA;AAChC,EAAA;AAAA;AAGsC,EAAA;AACV,IAAA;AACK,IAAA;AACtB,IAAA;AACsB,MAAA;AACA,MAAA;AAC/B,IAAA;AAC+B,IAAA;AACX,IAAA;AACc,IAAA;AACP,IAAA;AACT,IAAA;AACW,IAAA;AACI,IAAA;AACL,IAAA;AACA,IAAA;AAClB,IAAA;AACkC,MAAA;AAEV,MAAA;AACrB,MAAA;AACmB,MAAA;AAChC,IAAA;AAC4C,IAAA;AACX,IAAA;AACF,IAAA;AACC,IAAA;AAClC,EAAA;AAAA;AAG6E,EAAA;AAChD,IAAA;AACA,IAAA;AACA,IAAA;AACT,MAAA;AACe,MAAA;AACjC,IAAA;AACkC,IAAA;AACF,IAAA;AACD,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMmF,EAAA;AACvD,IAAA;AACF,IAAA;AACA,IAAA;AACK,IAAA;AACF,IAAA;AACK,IAAA;AACR,IAAA;AACS,IAAA;AACL,IAAA;AACF,IAAA;AACD,IAAA;AACM,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AACD,IAAA;AACA,IAAA;AACP,IAAA;AACD,IAAA;AACM,MAAA;AACG,MAAA;AACH,MAAA;AACC,MAAA;AAChC,IAAA;AAC2B,IAAA;AACnB,MAAA;AACa,MAAA;AACS,MAAA;AAC5B,MAAA;AACA,MAAA;AACgC,MAAA;AACrB,MAAA;AACD,MAAA;AACZ,IAAA;AACqB,IAAA;AACU,IAAA;AACC,IAAA;AAClC,EAAA;AAEmF,EAAA;AAClE,IAAA;AACiB,IAAA;AACP,IAAA;AACG,MAAA;AACI,MAAA;AACN,MAAA;AACK,MAAA;AACR,MAAA;AACvB,IAAA;AACkC,IAAA;AACpC,EAAA;AAAA;AAIsD,EAAA;AACvB,IAAA;AAC/B,EAAA;AAAA;AAG6C,EAAA;AAChB,IAAA;AACO,IAAA;AACpC,EAAA;AAE6C,EAAA;AAChB,IAAA;AACT,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiF,EAAA;AACpD,IAAA;AACU,IAAA;AAEF,IAAA;AACF,IAAA;AAEA,IAAA;AAChB,IAAA;AACJ,IAAA;AACD,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAGuF,EAAA;AACrD,IAAA;AACE,IAAA;AACH,IAAA;AACG,IAAA;AACrB,IAAA;AACc,IAAA;AACO,IAAA;AACF,IAAA;AACjB,IAAA;AACH,IAAA;AACL,IAAA;AACT,EAAA;AAE8B,EAAA;AACM,IAAA;AACP,IAAA;AACN,IAAA;AACO,IAAA;AACb,IAAA;AACH,IAAA;AACd,EAAA;AAAA;AAG+B,EAAA;AACA,IAAA;AAC/B,EAAA;AAAA;AAGyD,EAAA;AAC3B,IAAA;AACX,IAAA;AACnB,EAAA;AAAA;AAAA;AAAA;AAAA;AAM8H,EAAA;AAChG,IAAA;AACxB,IAAA;AACA,IAAA;AAC6B,MAAA;AACrB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACyB,IAAA;AACE,IAAA;AACL,IAAA;AACY,MAAA;AACb,QAAA;AACK,QAAA;AACtB,QAAA;AACF,MAAA;AAC2B,MAAA;AACK,MAAA;AACb,QAAA;AACjB,QAAA;AACF,MAAA;AAC8B,MAAA;AACF,MAAA;AACb,MAAA;AACjB,IAAA;AACkB,IAAA;AACD,MAAA;AACH,MAAA;AACd,IAAA;AACkC,IAAA;AACpC,EAAA;AAAA;AAG8G,EAAA;AAC3E,IAAA;AACA,IAAA;AACnC,EAAA;AAAA;AAAA;AAKgC,EAAA;AACD,IAAA;AACb,IAAA;AACJ,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQmD,EAAA;AAC7C,IAAA;AACA,IAAA;AACwB,MAAA;AAChB,IAAA;AACO,MAAA;AACX,MAAA;AACR,IAAA;AACwB,IAAA;AACD,MAAA;AACd,MAAA;AACT,IAAA;AACgB,IAAA;AACJ,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO4G,EAAA;AAC7E,IAAA;AACO,IAAA;AACN,IAAA;AACG,MAAA;AACC,MAAA;AACF,IAAA;AACA,IAAA;AAClC,EAAA;AAEmD,EAAA;AAC7C,IAAA;AACyB,MAAA;AACjB,IAAA;AACO,MAAA;AACX,MAAA;AACR,IAAA;AACF,EAAA;AAEyC,EAAA;AACN,IAAA;AACrB,IAAA;AACA,IAAA;AACd,EAAA;AAEmB,EAAA;AACe,IAAA;AACpB,IAAA;AACd,EAAA;AAAA;AAGsG,EAAA;AACrE,IAAA;AACR,IAAA;AACA,IAAA;AACU,IAAA;AACE,IAAA;AACL,MAAA;AACI,MAAA;AACD,MAAA;AACC,MAAA;AAClC,IAAA;AACgC,IAAA;AACE,IAAA;AACpC,EAAA;AAAA;AAAA;AAK0C,EAAA;AACZ,IAAA;AACI,IAAA;AACpB,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAGiC,EAAA;AACG,IAAA;AACtB,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAAA;AAK+C,EAAA;AACX,IAAA;AACtB,IAAA;AACA,IAAA;AACd,EAAA;AAEuB,EAAA;AACI,IAAA;AAC3B,EAAA;AAE0B,EAAA;AACQ,IAAA;AACpB,IAAA;AACA,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAIuD,EAAA;AACxB,IAAA;AACL,IAAA;AACjB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAM2D,EAAA;AACzB,IAAA;AACD,IAAA;AACjC,EAAA;AAAA;AAGmC,EAAA;AACC,IAAA;AACpC,EAAA;AAEgH,EAAA;AAChF,IAAA;AACG,MAAA;AACA,MAAA;AACC,MAAA;AAClC,IAAA;AACmB,IAAA;AACc,MAAA;AACZ,MAAA;AACO,MAAA;AACN,MAAA;AACN,MAAA;AACkB,MAAA;AACA,MAAA;AACN,MAAA;AACnB,MAAA;AACN,IAAA;AAC4B,IAAA;AACE,IAAA;AACD,IAAA;AACT,MAAA;AACE,IAAA;AACI,MAAA;AAC7B,IAAA;AACI,IAAA;AACmB,IAAA;AACS,MAAA;AACF,MAAA;AAEiB,QAAA;AACjB,QAAA;AACI,QAAA;AACtB,QAAA;AACV,MAAA;AAC4B,MAAA;AACC,QAAA;AAC7B,MAAA;AACgB,MAAA;AACO,MAAA;AACN,MAAA;AACe,MAAA;AACJ,MAAA;AACH,MAAA;AACC,MAAA;AACC,MAAA;AACK,MAAA;AACb,MAAA;AACjB,QAAA;AACQ,QAAA;AACR,QAAA;AAC8B,QAAA;AAC9B,QAAA;AAC2B,QAAA;AACL,QAAA;AACI,QAAA;AACpB,QAAA;AACuB,QAAA;AACH,QAAA;AACnB,QAAA;AACR,MAAA;AAC8B,MAAA;AACA,MAAA;AACC,IAAA;AAEF,MAAA;AACD,MAAA;AACrB,MAAA;AACV,IAAA;AAC2B,IAAA;AACC,IAAA;AACS,IAAA;AACL,IAAA;AACX,IAAA;AACI,IAAA;AACF,IAAA;AACA,IAAA;AACQ,IAAA;AACN,IAAA;AACQ,IAAA;AACF,IAAA;AACX,IAAA;AACH,MAAA;AACA,MAAA;AACD,MAAA;AACkB,MAAA;AAChC,MAAA;AACO,MAAA;AACsB,MAAA;AACF,MAAA;AACL,MAAA;AACtB,MAAA;AACU,MAAA;AACI,MAAA;AACe,MAAA;AAC7B,MAAA;AACA,MAAA;AACwB,MAAA;AACD,MAAA;AACvB,MAAA;AACwB,MAAA;AACE,MAAA;AACC,MAAA;AAC5B,IAAA;AAC8B,IAAA;AACA,IAAA;AACxB,IAAA;AACT,EAAA;AAE4D,EAAA;AACrC,IAAA;AACU,IAAA;AACD,MAAA;AACX,MAAA;AACY,MAAA;AACjB,MAAA;AACgB,MAAA;AACjB,MAAA;AACb,IAAA;AAC8B,IAAA;AACC,IAAA;AACC,IAAA;AACC,IAAA;AACb,IAAA;AACK,MAAA;AACE,MAAA;AAC3B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAM4D,EAAA;AACzC,IAAA;AACG,IAAA;AACY,IAAA;AACH,MAAA;AACG,QAAA;AAEJ,UAAA;AACtB,UAAA;AACF,QAAA;AACuB,QAAA;AACE,QAAA;AACD,QAAA;AACT,QAAA;AACjB,MAAA;AACqB,MAAA;AACJ,QAAA;AACH,QAAA;AACM,QAAA;AACD,QAAA;AACW,QAAA;AACJ,QAAA;AACH,QAAA;AACO,QAAA;AACD,QAAA;AACT,UAAA;AAEU,YAAA;AACX,YAAA;AACY,YAAA;AACD,YAAA;AACE,YAAA;AACJ,YAAA;AACxB,UAAA;AACU,UAAA;AACV,UAAA;AACF,QAAA;AAC4B,QAAA;AACA,QAAA;AACF,QAAA;AACF,UAAA;AACK,UAAA;AACA,UAAA;AACF,UAAA;AAC3B,QAAA;AAC0B,QAAA;AACC,QAAA;AACG,QAAA;AACH,QAAA;AACE,QAAA;AACJ,QAAA;AAC3B,MAAA;AACF,IAAA;AACI,IAAA;AACA,IAAA;AAC6B,MAAA;AACrB,IAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACe,IAAA;AACH,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG8C,EAAA;AAChC,IAAA;AACd,EAAA;AAAA;AAG0K,EAAA;AAC9J,IAAA;AACsB,IAAA;AAChB,MAAA;AAAC,MAAA;AACM,MAAA;AACQ,QAAA;AACC,QAAA;AACA,QAAA;AACJ,QAAA;AACM,QAAA;AACH,QAAA;AACA,QAAA;AACrB,QAAA;AACD,UAAA;AACkB,UAAA;AACK,UAAA;AACE,UAAA;AACA,UAAA;AAC9B,QAAA;AACF,MAAA;AACF,IAAA;AACgC,IAAA;AACb,IAAA;AACQ,IAAA;AACL,IAAA;AACF,MAAA;AACS,MAAA;AACG,QAAA;AACD,QAAA;AACG,QAAA;AACN,QAAA;AAC1B,MAAA;AAC4B,MAAA;AAC9B,IAAA;AACkC,IAAA;AACpC,EAAA;AAAA;AAGwF,EAAA;AAC3D,IAAA;AACK,IAAA;AACZ,IAAA;AAChB,IAAA;AACA,IAAA;AAC6B,MAAA;AACrB,IAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AAC+B,IAAA;AACJ,IAAA;AACO,IAAA;AACjB,IAAA;AACV,IAAA;AACT,EAAA;AAAA;AAGqD,EAAA;AACpB,IAAA;AACN,IAAA;AACA,IAAA;AACO,MAAA;AACI,MAAA;AACxB,MAAA;AACX,IAAA;AACM,IAAA;AACT,EAAA;AAAA;AAGwC,EAAA;AACtB,IAAA;AAClB,EAAA;AAAA;AAAA;AAAA;AAAA;AAM8G,EAAA;AAChF,IAAA;AACI,IAAA;AACE,IAAA;AACA,MAAA;AACd,MAAA;AACX,MAAA;AACR,IAAA;AACgC,IAAA;AACA,IAAA;AACI,IAAA;AACJ,IAAA;AACE,IAAA;AACA,IAAA;AACd,IAAA;AACR,IAAA;AACoB,MAAA;AACD,MAAA;AAChC,IAAA;AAC6B,IAAA;AACd,IAAA;AACiB,IAAA;AACT,IAAA;AACN,IAAA;AACD,IAAA;AACiB,IAAA;AACC,MAAA;AACP,MAAA;AACI,QAAA;AACb,QAAA;AACU,QAAA;AACM,UAAA;AACA,UAAA;AAC9B,QAAA;AACwB,QAAA;AACG,QAAA;AAEd,QAAA;AACiB,QAAA;AACA,UAAA;AACV,UAAA;AACpB,QAAA;AAEyB,QAAA;AACE,QAAA;AACjB,QAAA;AACiB,QAAA;AACE,QAAA;AACT,QAAA;AACtB,MAAA;AACF,IAAA;AACkC,IAAA;AACD,IAAA;AACnC,EAAA;AAAA;AAAA;AAKwF,EAAA;AACnE,IAAA;AACc,IAAA;AACd,IAAA;AACc,IAAA;AACb,IAAA;AACtB,EAAA;AAAA;AAG6B,EAAA;AACD,IAAA;AAC5B,EAAA;AAAA;AAG2D,EAAA;AAChC,IAAA;AACY,IAAA;AACH,IAAA;AACnB,IAAA;AACW,IAAA;AAC5B,EAAA;AAAA;AAGqG,EAAA;AACpE,IAAA;AACf,IAAA;AACI,IAAA;AACU,IAAA;AACd,IAAA;AACO,IAAA;AACS,IAAA;AAClC,EAAA;AAAA;AAGyE,EAAA;AAC1C,IAAA;AACF,IAAA;AACO,IAAA;AACA,IAAA;AACJ,IAAA;AACN,IAAA;AAC1B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASqD,EAAA;AAClB,IAAA;AACF,IAAA;AACG,IAAA;AACJ,IAAA;AACI,IAAA;AACf,IAAA;AACS,IAAA;AACI,IAAA;AACJ,IAAA;AACrB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOoE,EAAA;AAC9C,IAAA;AAChB,IAAA;AAC0B,MAAA;AAC5B,IAAA;AACe,MAAA;AACjB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQyF,EAAA;AAChE,IAAA;AACS,IAAA;AAClC,EAAA;AAEsF,EAAA;AAChE,IAAA;AAChB,IAAA;AAC6B,MAAA;AACV,MAAA;AACW,MAAA;AAChC,IAAA;AACe,MAAA;AACjB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQoC,EAAA;AACV,IAAA;AAC1B,EAAA;AAAA;AAG2C,EAAA;AACR,IAAA;AACnC,EAAA;AAAA;AAGgB,EAAA;AACG,IAAA;AACnB,EAAA;AAAA;AAGyB,EAAA;AACF,IAAA;AACvB,EAAA;AAE6E,EAAA;AAC5C,IAAA;AACZ,IAAA;AACW,IAAA;AACE,IAAA;AACzB,IAAA;AACT,EAAA;AAEwE,EAAA;AACpD,IAAA;AACa,IAAA;AACG,IAAA;AACF,IAAA;AACL,IAAA;AACpB,IAAA;AACT,EAAA;AAAA;AAG0I,EAAA;AAC7G,IAAA;AACM,IAAA;AACA,IAAA;AACnC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO2B,EAAA;AACE,IAAA;AAC7B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ6F,EAAA;AAC3D,IAAA;AACE,IAAA;AACT,IAAA;AACI,IAAA;AACnB,IAAA;AACkB,IAAA;AAClB,IAAA;AACH,IAAA;AACT,EAAA;AAAA;AAG6C,EAAA;AACjB,IAAA;AAC5B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOsJ,EAAA;AACrH,IAAA;AACE,IAAA;AACN,IAAA;AACjB,IAAA;AACe,IAAA;AACM,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOqJ,EAAA;AAC1H,IAAA;AACG,IAAA;AACO,IAAA;AACP,IAAA;AACI,IAAA;AACE,IAAA;AACd,IAAA;AACJ,IAAA;AACkB,IAAA;AACP,IAAA;AACG,IAAA;AACC,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsI,EAAA;AACnH,IAAA;AACe,IAAA;AACA,IAAA;AACC,IAAA;AACC,IAAA;AACN,IAAA;AACA,IAAA;AACR,IAAA;AACJ,IAAA;AACkB,IAAA;AACD,IAAA;AACX,IAAA;AACW,IAAA;AACH,IAAA;AACA,IAAA;AAChC,EAAA;AAAA;AAG2D,EAAA;AAC/B,IAAA;AAC5B,EAAA;AAAA;AAGiF,EAAA;AAC9C,IAAA;AACb,IAAA;AACc,IAAA;AACJ,IAAA;AAClB,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGmF,EAAA;AAChD,IAAA;AACb,IAAA;AACY,IAAA;AACF,IAAA;AAClB,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGiD,EAAA;AAC3B,IAAA;AACgB,IAAA;AACN,IAAA;AACA,IAAA;AAClB,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGuC,EAAA;AACnB,IAAA;AACN,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGmC,EAAA;AACG,IAAA;AACnB,IAAA;AACL,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGoC,EAAA;AACZ,IAAA;AACV,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG2C,EAAA;AACtB,IAAA;AACP,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGgG,EAAA;AACnE,IAAA;AACM,IAAA;AACnC,EAAA;AAAA;AAIkB,EAAA;AACgB,IAAA;AAClC,EAAA;AAAA;AAGsC,EAAA;AAClB,IAAA;AACS,IAAA;AACO,IAAA;AACD,IAAA;AACH,IAAA;AACvB,IAAA;AACT,EAAA;AAAA;AAGwC,EAAA;AAChB,IAAA;AACQ,IAAA;AACH,IAAA;AAC7B,EAAA;AAAA;AAGgD,EAAA;AACf,IAAA;AACG,IAAA;AACH,IAAA;AACE,IAAA;AACC,IAAA;AACW,IAAA;AACb,IAAA;AACzB,IAAA;AACT,EAAA;AAAA;AAGgD,EAAA;AACb,IAAA;AACT,IAAA;AACO,IAAA;AACrB,IAAA;AACY,IAAA;AACG,MAAA;AACU,MAAA;AACnC,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGsD,EAAA;AAC7B,IAAA;AACL,IAAA;AACU,IAAA;AAClB,IAAA;AACM,IAAA;AACM,IAAA;AACS,MAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AACA,MAAA;AACP,MAAA;AACK,MAAA;AAC/B,IAAA;AACO,IAAA;AACT,EAAA;AAEwE,EAAA;AAC3B,IAAA;AACrB,IAAA;AACU,MAAA;AACL,MAAA;AACK,MAAA;AAChC,IAAA;AAC6C,IAAA;AAChB,IAAA;AACF,IAAA;AAC7B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOsH,EAAA;AACpF,IAAA;AACZ,IAAA;AACtB,EAAA;AAAA;AAGyD,EAAA;AACxB,IAAA;AACb,IAAA;AACW,IAAA;AAC/B,EAAA;AAAA;AAG8E,EAAA;AAC1C,IAAA;AACJ,IAAA;AACI,IAAA;AACf,IAAA;AACc,IAAA;AACA,IAAA;AACH,IAAA;AAChC,EAAA;AAAA;AAAA;AAAA;AAMyC,EAAA;AACP,IAAA;AACL,IAAA;AAC7B,EAAA;AAAA;AAG2B,EAAA;AACyB,IAAA;AACjB,IAAA;AACN,IAAA;AACF,IAAA;AACS,IAAA;AACpC,EAAA;AAEwD,EAAA;AACvC,IAAA;AACY,IAAA;AACjB,IAAA;AACwB,IAAA;AACT,IAAA;AACM,MAAA;AACJ,QAAA;AACP,UAAA;AACd,UAAA;AACF,QAAA;AACU,QAAA;AACkB,QAAA;AAC9B,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGmD,EAAA;AACS,IAAA;AACf,IAAA;AACb,IAAA;AACI,MAAA;AACJ,MAAA;AACF,MAAA;AAC5B,IAAA;AACkC,IAAA;AAClB,IAAA;AAC6B,IAAA;AACb,IAAA;AACE,IAAA;AAClB,IAAA;AACc,IAAA;AACd,IAAA;AACT,IAAA;AACT,EAAA;AAAA;AAGmH,EAAA;AAC9E,IAAA;AACd,IAAA;AACU,IAAA;AACgB,MAAA;AACb,MAAA;AACD,QAAA;AACgB,QAAA;AACd,QAAA;AACD,QAAA;AAChC,MAAA;AAC0B,MAAA;AAC5B,IAAA;AACgB,IAAA;AACW,IAAA;AACmB,IAAA;AACjB,IAAA;AACI,MAAA;AAC3B,MAAA;AACyB,QAAA;AACjB,MAAA;AACI,QAAA;AAChB,MAAA;AACF,IAAA;AAC0B,IAAA;AAC5B,EAAA;AAEqE,EAAA;AACrC,IAAA;AAChC,EAAA;AAAA;AAG8G,EAAA;AACrG,IAAA;AACT,EAAA;AAEkM,EAAA;AACvK,IAAA;AACO,IAAA;AACN,IAAA;AACK,IAAA;AACE,IAAA;AACJ,IAAA;AACC,MAAA;AACC,MAAA;AAC/B,IAAA;AACkB,IAAA;AACA,IAAA;AACe,IAAA;AACV,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AAChB,IAAA;AACI,IAAA;AAChB,EAAA;AAAA;AAGiC,EAAA;AACd,IAAA;AACb,IAAA;AACgB,MAAA;AACR,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAG8E,EAAA;AAC3D,IAAA;AACb,IAAA;AACsB,MAAA;AACd,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAGwE,EAAA;AACvC,IAAA;AACC,IAAA;AAClC,EAAA;AAEwE,EAAA;AACpC,IAAA;AACP,IAAA;AACO,IAAA;AACd,IAAA;AACa,IAAA;AACA,IAAA;AACH,IAAA;AAChC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiI,EAAA;AAC9F,IAAA;AACL,IAAA;AACG,IAAA;AACP,IAAA;AACS,IAAA;AACJ,IAAA;AACT,IAAA;AACG,IAAA;AACW,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO6B,EAAA;AACO,IAAA;AACH,IAAA;AACN,IAAA;AACW,IAAA;AAC5B,MAAA;AACF,MAAA;AACE,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACY,MAAA;AACQ,MAAA;AACN,MAAA;AACvB,IAAA;AACoB,IAAA;AAEJ,IAAA;AAEdC,IAAAA;AAC4B,MAAA;AACK,MAAA;AACN,MAAA;AACM,MAAA;AAChC,IAAA;AAC4B,IAAA;AACG,IAAA;AACN,IAAA;AACM,MAAA;AACF,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AAC/B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGsH,EAAA;AACrF,IAAA;AACF,IAAA;AAC/B,EAAA;AAAA;AAIkD,EAAA;AAClC,IAAA;AACC,IAAA;AACR,IAAA;AACT,EAAA;AAAA;AAG2D,EAAA;AACxC,IAAA;AACgB,IAAA;AAC1B,IAAA;AACT,EAAA;AAAA;AAGoE,EAAA;AAC9D,IAAA;AACA,IAAA;AACY,MAAA;AACJ,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AAC1B,IAAA;AACT,EAAA;AAAA;AAGqH,EAAA;AAC/G,IAAA;AACc,IAAA;AACgB,MAAA;AACJ,MAAA;AACI,QAAA;AAChC,MAAA;AACgC,MAAA;AACF,IAAA;AACE,MAAA;AACA,IAAA;AACf,IAAA;AACW,IAAA;AACE,IAAA;AACX,IAAA;AACvB,EAAA;AAE6D,EAAA;AAC3B,IAAA;AACb,IAAA;AACY,IAAA;AACR,IAAA;AACnB,IAAA;AAC2B,MAAA;AACE,MAAA;AACH,MAAA;AACrB,MAAA;AACG,IAAA;AACoB,MAAA;AAChC,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMqJ,EAAA;AACtH,IAAA;AACE,IAAA;AACG,IAAA;AACD,IAAA;AACW,IAAA;AACb,IAAA;AACjC,EAAA;AAAA;AAGgH,EAAA;AACzF,IAAA;AACe,IAAA;AACH,IAAA;AACH,IAAA;AACzB,MAAA;AACgC,MAAA;AACA,MAAA;AACP,MAAA;AACG,MAAA;AACjC,IAAA;AACgC,IAAA;AACE,IAAA;AACD,IAAA;AAC1B,IAAA;AACT,EAAA;AAE0B,EAAA;AACnB,IAAA;AAC4B,IAAA;AACnC,EAAA;AAAA;AAGwD,EAAA;AAChC,IAAA;AACP,IAAA;AACmB,IAAA;AACA,IAAA;AACA,IAAA;AACH,IAAA;AACC,IAAA;AACE,IAAA;AACD,IAAA;AACnC,EAAA;AAAA;AAGgJ,EAAA;AACjH,IAAA;AACD,IAAA;AACM,MAAA;AACF,MAAA;AAC9B,MAAA;AACc,MAAA;AACC,MAAA;AACW,MAAA;AACA,MAAA;AACJ,MAAA;AACb,MAAA;AACX,IAAA;AACiC,IAAA;AAC1B,IAAA;AACT,EAAA;AAEqD,EAAA;AAClB,IAAA;AACf,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAGyI,EAAA;AACrG,IAAA;AACN,IAAA;AACmB,IAAA;AACd,IAAA;AAC1B,IAAA;AACT,EAAA;AAEuC,EAAA;AACb,IAAA;AACS,IAAA;AACnC,EAAA;AAAA;AAGwC,EAAA;AACT,IAAA;AAC/B,EAAA;AAAA;AAG6D,EAAA;AACzB,IAAA;AACD,IAAA;AACX,IAAA;AACA,IAAA;AACW,IAAA;AACF,IAAA;AACA,IAAA;AACjC,EAAA;AAEsF,EAAA;AACnD,IAAA;AACnB,IAAA;AACT,IAAA;AACsB,IAAA;AACjB,IAAA;AAC4B,IAAA;AACpB,IAAA;AACd,IAAA;AACmB,MAAA;AACX,IAAA;AACI,MAAA;AAChB,IAAA;AACY,IAAA;AACc,IAAA;AACpB,MAAA;AAC4B,QAAA;AACb,QAAA;AACR,UAAA;AACG,UAAA;AACC,UAAA;AACY,UAAA;AACL,UAAA;AACK,UAAA;AACP,UAAA;AAClB,QAAA;AACU,MAAA;AACwB,QAAA;AACpC,MAAA;AACF,IAAA;AACiC,IAAA;AACqB,MAAA;AAC3B,QAAA;AACnB,QAAA;AAC0B,UAAA;AACH,UAAA;AACR,UAAA;AACH,UAAA;AACJ,QAAA;AACwB,UAAA;AACpC,QAAA;AACF,MAAA;AACF,IAAA;AAC+B,IAAA;AACV,MAAA;AAC2B,MAAA;AACxC,QAAA;AACqB,UAAA;AACF,UAAA;AACO,UAAA;AACC,UAAA;AACnB,QAAA;AACe,UAAA;AAC3B,QAAA;AACD,MAAA;AACH,IAAA;AACc,IAAA;AAChB,EAAA;AAAA;AAAA;AAK0F,EAAA;AACrE,IAAA;AACc,IAAA;AACd,IAAA;AACc,IAAA;AACV,IAAA;AACzB,EAAA;AAAA;AAGsE,EAAA;AAC5D,IAAA;AACyB,IAAA;AACtB,IAAA;AACb,EAAA;AAEmG,EAAA;AAChE,IAAA;AACP,IAAA;AACD,IAAA;AACG,IAAA;AAC9B,EAAA;AAAA;AAG0C,EAAA;AACT,IAAA;AACd,IAAA;AACG,IAAA;AACU,IAAA;AAChC,EAAA;AAAA;AAG8D,EAAA;AAC3B,IAAA;AACF,IAAA;AACJ,IAAA;AACH,IAAA;AAC1B,EAAA;AAAA;AAGsG,EAAA;AACnE,IAAA;AACH,IAAA;AACT,IAAA;AACjB,IAAA;AACA,IAAA;AACyB,MAAA;AACjB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACkC,IAAA;AACd,IAAA;AACH,IAAA;AACnB,EAAA;AAAA;AAGqG,EAAA;AAClE,IAAA;AACN,IAAA;AACK,IAAA;AACF,IAAA;AACI,IAAA;AACd,IAAA;AACO,IAAA;AAC7B,EAAA;AAAA;AAG+E,EAAA;AAC5C,IAAA;AAC7B,IAAA;AAAgC,MAAA;AAA6I,IAAA;AAChJ,IAAA;AACb,IAAA;AACb,IAAA;AACT,EAAA;AAAA;AAGqE,EAAA;AAClC,IAAA;AACD,IAAA;AAClC,EAAA;AAAA;AAIgI,oBAAA;AACxG,oBAAA;AAAA;AAGqC,EAAA;AAC5B,IAAA;AACA,IAAA;AACA,IAAA;AACN,IAAA;AACE,IAAA;AACpB,IAAA;AACT,EAAA;AAAA;AAG0B,EAAA;AACS,IAAA;AACnC,EAAA;AAAA;AAGyC,EAAA;AACnC,IAAA;AACA,IAAA;AACyB,MAAA;AACjB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AACb,IAAA;AACtB,EAAA;AAE4C,EAAA;AACX,IAAA;AACb,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAGkF,EAAA;AAC/C,IAAA;AAC7B,IAAA;AACA,IAAA;AAC2B,MAAA;AACnB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AAChB,IAAA;AACnB,EAAA;AAAA;AAGkD,EAAA;AACjB,IAAA;AACE,IAAA;AACA,IAAA;AAC1B,IAAA;AACT,EAAA;AAAA;AAAA;AAKkH,EAAA;AAC5G,IAAA;AAC0B,MAAA;AAClB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAGgE,EAAA;AACrC,IAAA;AACb,IAAA;AACA,IAAA;AACd,EAAA;AAEuE,EAAA;AAC1C,IAAA;AACE,IAAA;AACK,IAAA;AACH,IAAA;AACxB,IAAA;AACT,EAAA;AAEsC,EAAA;AACJ,IAAA;AACF,IAAA;AAClB,IAAA;AACA,IAAA;AACd,EAAA;AAEmD,EAAA;AACnB,IAAA;AAClB,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAI2B,EAAA;AACb,IAAA;AACd,EAAA;AAAA;AAGiH,EAAA;AAC/E,IAAA;AACI,IAAA;AAChC,IAAA;AACwB,MAAA;AACd,MAAA;AACL,MAAA;AACG,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAG0G,EAAA;AACpG,IAAA;AACA,IAAA;AAC8B,MAAA;AACtB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACgC,IAAA;AACzB,IAAA;AACT,EAAA;AAEuB,EAAA;AACN,IAAA;AACH,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO2J,EAAA;AACvH,IAAA;AAC9B,IAAA;AACA,IAAA;AAC8B,MAAA;AACtB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AACrB,IAAA;AACe,IAAA;AACM,IAAA;AACnC,EAAA;AAAA;AAGuB,EAAA;AACC,IAAA;AACxB,EAAA;AAAA;AAGmC,EAAA;AAC7B,IAAA;AACkB,MAAA;AACV,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAGuC,EAAA;AACJ,IAAA;AACnC,EAAA;AAAA;AAGiD,EAAA;AACjB,IAAA;AACC,IAAA;AACc,IAAA;AACtB,IAAA;AACX,IAAA;AACmB,IAAA;AACjC,EAAA;AAE+C,EAAA;AACd,IAAA;AACb,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAG8C,EAAA;AAChB,IAAA;AACF,IAAA;AACM,IAAA;AAClC,EAAA;AAAA;AAGgD,EAAA;AAClB,IAAA;AACI,IAAA;AACpB,IAAA;AACoB,IAAA;AAClC,EAAA;AAEmF,EAAA;AACrD,IAAA;AACO,IAAA;AACrC,EAAA;AAE8D,EAAA;AAC1C,IAAA;AACD,IAAA;AACF,IAAA;AACkB,IAAA;AACvB,IAAA;AACe,IAAA;AACnB,MAAA;AACwB,QAAA;AAChB,MAAA;AACI,QAAA;AAChB,MAAA;AACF,IAAA;AAC4B,IAAA;AACA,MAAA;AACd,QAAA;AACiB,QAAA;AACX,UAAA;AACd,UAAA;AACF,QAAA;AAC2B,QAAA;AACC,QAAA;AACE,QAAA;AACJ,QAAA;AAC3B,MAAA;AACH,IAAA;AACF,EAAA;AAAA;AAG8C,EAAA;AACnB,IAAA;AACE,IAAA;AAC7B,EAAA;AACF;AAK8B;AAGwE;AAClE,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACL,EAAA;AACC,EAAA;AACf,EAAA;AACgB,EAAA;AACI,IAAA;AACT,IAAA;AACU,IAAA;AAClB,IAAA;AACe,IAAA;AACG,MAAA;AACC,MAAA;AACA,MAAA;AACR,MAAA;AACQ,MAAA;AAChB,MAAA;AACY,MAAA;AACA,MAAA;AACI,QAAA;AACA,QAAA;AACH,QAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AACkC,EAAA;AACpC;AlB2oFsC;AACA;AmBjzKH;AAAe;AAwBpC;AAGwD;AACtC,EAAA;AACR,EAAA;AACa,EAAA;AACJ,EAAA;AACK,IAAA;AACpC,EAAA;AACc,EAAA;AACqB,EAAA;AACH,EAAA;AACE,EAAA;AACpC;AAGkE;AAC1C,EAAA;AACa,EAAA;AAC/B,EAAA;AAC2B,EAAA;AAC1B,EAAA;AACU,IAAA;AACc,IAAA;AACb,IAAA;AAChB,EAAA;AACmC,EAAA;AACrC;AAGuF;AAClD,EAAA;AACb,EAAA;AACc,EAAA;AACU,EAAA;AACf,EAAA;AACK,IAAA;AACH,IAAA;AACjC,EAAA;AACO,EAAA;AACT;AAEgD;AAUrB;AAON,EAAA;AAFA,IAAA;AACR,IAAA;AACQ,IAAA;AAEa,IAAA;AACI,IAAA;AACpC,EAAA;AANmB,EAAA;AACR,EAAA;AACQ,EAAA;AANF,EAAA;AACA,EAAA;AAAA;AAY4F,EAAA;AAC9E,IAAA;AACgB,MAAA;AACX,MAAA;AAC1B,QAAA;AACuB,UAAA;AACX,QAAA;AACY,UAAA;AAC5B,QAAA;AACF,MAAA;AACO,MAAA;AACR,IAAA;AACH,EAAA;AAE8B,EAAA;AACG,IAAA;AACjC,EAAA;AAE0B,EAAA;AACM,IAAA;AACI,IAAA;AACpC,EAAA;AAAA;AAGwE,EAAA;AACxC,IAAA;AACM,IAAA;AACJ,MAAA;AAC9B,MAAA;AACF,IAAA;AAC+B,IAAA;AACjC,EAAA;AAEoE,EAAA;AACvD,IAAA;AACsB,IAAA;AACA,IAAA;AACnC,EAAA;AAEmF,EAAA;AAClE,IAAA;AACO,IAAA;AACf,MAAA;AACwB,QAAA;AACpB,QAAA;AACJ,MAAA;AACwB,QAAA;AACE,QAAA;AACL,QAAA;AACM,QAAA;AACN,QAAA;AACjB,QAAA;AACJ,MAAA;AACwB,QAAA;AACJ,QAAA;AACZ,MAAA;AACgB,QAAA;AACC,QAAA;AACA,QAAA;AACC,QAAA;AACL,QAAA;AACpB,QAAA;AACuB,UAAA;AACzB,QAAA;AACwB,UAAA;AAC1B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACe,MAAA;AACc,QAAA;AACG,QAAA;AACL,QAAA;AACG,QAAA;AACX,QAAA;AACa,QAAA;AACvB,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGsH,EAAA;AACrG,IAAA;AACmB,IAAA;AACD,IAAA;AAED,IAAA;AAC5B,IAAA;AAC6B,MAAA;AACP,MAAA;AACQ,MAAA;AACP,MAAA;AACd,MAAA;AACqB,MAAA;AACH,MAAA;AACb,QAAA;AACD,QAAA;AACQ,QAAA;AACJ,QAAA;AACW,UAAA;AACD,UAAA;AACE,UAAA;AAC7B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AAC0B,MAAA;AACF,MAAA;AACL,MAAA;AACL,IAAA;AACkB,MAAA;AACR,MAAA;AAClB,MAAA;AACR,IAAA;AACF,EAAA;AACF;AnBswKsC;AACA;AoBl+KH;AACE;AACD;AACD;AAE4C;AACtE,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACO,MAAA;AAC/B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACD,UAAA;AACa,UAAA;AACG,UAAA;AACJ,UAAA;AACA,UAAA;AACG,UAAA;AACI,UAAA;AACH,UAAA;AACE,UAAA;AAC7B,QAAA;AACe,QAAA;AACO,QAAA;AACxB,MAAA;AACsB,MAAA;AACM,MAAA;AAC9B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACD,UAAA;AACa,UAAA;AACM,UAAA;AAC9B,QAAA;AACsB,QAAA;AACA,QAAA;AACxB,MAAA;AACsB,MAAA;AACO,MAAA;AAC/B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACU,MAAA;AAClC,IAAA;AACF,EAAA;AACF;ApBm+KsC;AACA;AqBhiLH;AACE;AACF;AACC;AACJ;AACA;AAClB;AACN,EAAA;AAC6B,EAAA;AAChB,EAAA;AACG,EAAA;AACxB;AACa;AACL,EAAA;AACO,EAAA;AACD,EAAA;AACwB,IAAA;AACA,IAAA;AACT,IAAA;AACC,IAAA;AAC5B,EAAA;AAC8B,EAAA;AACR,EAAA;AACxB;AAC8B;AAEgC;AACP;AAEsB;AACzC,EAAA;AACT,EAAA;AACQ,IAAA;AACH,IAAA;AACG,IAAA;AACF,IAAA;AACI,IAAA;AACP,IAAA;AACI,IAAA;AACF,IAAA;AACI,IAAA;AACF,IAAA;AACL,IAAA;AACE,IAAA;AACM,IAAA;AACN,IAAA;AAC7B,EAAA;AACO,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACI,QAAA;AACS,QAAA;AAC5B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACc,UAAA;AACJ,UAAA;AACQ,UAAA;AACV,UAAA;AACS,UAAA;AAC7B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACN,MAAA;AACW,QAAA;AACF,QAAA;AACzB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACkB,UAAA;AACH,UAAA;AACA,UAAA;AAC3B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACJ,MAAA;AACpB,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACL,UAAA;AACC,UAAA;AACqB,UAAA;AACL,UAAA;AACxB,QAAA;AACgB,QAAA;AACM,QAAA;AACxB,MAAA;AACsB,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACC,UAAA;AACN,UAAA;AAChB,UAAA;AACR,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACO,QAAA;AACG,QAAA;AACzB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACL,UAAA;AACmB,UAAA;AACH,UAAA;AACA,UAAA;AACC,UAAA;AACxB,QAAA;AACgB,QAAA;AACM,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACY,UAAA;AACE,UAAA;AACH,UAAA;AACA,UAAA;AACC,UAAA;AACM,UAAA;AAC9B,QAAA;AACe,QAAA;AACO,QAAA;AACxB,MAAA;AACsB,MAAA;AACK,MAAA;AAC7B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACD,MAAA;AACM,QAAA;AACJ,QAAA;AACvB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACY,UAAA;AACD,UAAA;AACA,UAAA;AACH,UAAA;AACpB,QAAA;AACyB,QAAA;AACH,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACb,QAAA;AACc,QAAA;AACf,QAAA;AACjB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACc,UAAA;AACL,UAAA;AACM,UAAA;AACC,UAAA;AAC5B,QAAA;AACiB,QAAA;AACK,QAAA;AACxB,MAAA;AACsB,MAAA;AACU,MAAA;AAClC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACS,UAAA;AACP,UAAA;AACY,UAAA;AAC1B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACE,QAAA;AACV,QAAA;AACvB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACK,MAAA;AAC7B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACF,UAAA;AACA,YAAA;AACI,YAAA;AACA,YAAA;AACG,YAAA;AACN,YAAA;AACC,cAAA;AACa,cAAA;AACA,cAAA;AACG,cAAA;AACxB,YAAA;AACF,UAAA;AACwB,UAAA;AACL,UAAA;AACK,UAAA;AAC1B,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACW,UAAA;AACA,UAAA;AACM,UAAA;AACT,UAAA;AACpB,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACI,QAAA;AACM,QAAA;AACzB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACF,UAAA;AACA,YAAA;AACI,YAAA;AACA,YAAA;AACG,YAAA;AACN,YAAA;AACC,cAAA;AACM,cAAA;AACS,gBAAA;AACJ,gBAAA;AACjB,cAAA;AACoB,cAAA;AACE,cAAA;AACxB,YAAA;AACF,UAAA;AACqB,UAAA;AACA,UAAA;AACK,UAAA;AACC,UAAA;AACD,UAAA;AACR,UAAA;AACpB,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACG,MAAA;AAC3B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACC,MAAA;AACzB,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACC,MAAA;AACzB,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACC,QAAA;AACP,QAAA;AACI,UAAA;AACgB,YAAA;AACD,YAAA;AACxB,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AAC6B,MAAA;AACT,MAAA;AAC8D,MAAA;AACtF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACU,MAAA;AAClC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACG,MAAA;AAC3B,IAAA;AACF,EAAA;AACF;ArByhLsC;AACA;AsB59LH;AACE;AACF;AACH;AACG;AACtB;AACL,EAAA;AACO,EAAA;AACD,EAAA;AACwB,IAAA;AACA,IAAA;AACT,IAAA;AACC,IAAA;AAC5B,EAAA;AAC8B,EAAA;AACR,EAAA;AACxB;AAE+C;AAEf;AACd,EAAA;AACE,EAAA;AACE,EAAA;AACA,EAAA;AACF,EAAA;AACL,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACd;AAE2E;AACrC,EAAA;AAC7B,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACWC,MAAAA;AACS,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACgB,UAAA;AACC,UAAA;AAC7B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACG,MAAA;AACP,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AAC6B,MAAA;AACT,MAAA;AACU,MAAA;AACd,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACiBC,QAAAA;AACD,QAAA;AACxB,MAAA;AACsB,MAAA;AACK,MAAA;AACT,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AAC6B,MAAA;AACT,MAAA;AACQ,MAAA;AACZ,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACDA,UAAAA;AACe,UAAA;AACT,UAAA;AACU,UAAA;AACjB,UAAA;AACA,YAAA;AACO,YAAA;AACY,YAAA;AACN,YAAA;AACG,YAAA;AACxB,UAAA;AACmB,UAAA;AACM,UAAA;AACA,UAAA;AAC3B,QAAA;AACiB,QAAA;AACK,QAAA;AACxB,MAAA;AACsB,MAAA;AACQ,MAAA;AACZ,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACe,UAAA;AACN,UAAA;AACrB,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACG,MAAA;AACP,QAAA;AACC,QAAA;AACnB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACF,UAAA;AAC3B,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACU,MAAA;AACT,QAAA;AACM,QAAA;AACJ,QAAA;AACI,QAAA;AAC7B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACa,UAAA;AACL,UAAA;AACpB,QAAA;AACgB,QAAA;AACM,QAAA;AACxB,MAAA;AACsB,MAAA;AACU,MAAA;AAClC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACO,MAAA;AAC/B,IAAA;AACF,EAAA;AACF;AtBm9LsC;AACA;AuBhqMH;AACA;AACE;AACF;AAE0C;AACpE,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACR,UAAA;AACO,UAAA;AAC5B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACjC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACO,MAAA;AAC/B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACM,MAAA;AAC9B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACE,MAAA;AAC1B,IAAA;AACF,EAAA;AACF;AvBgqMsC;AACA;AwBhtMH;AAE2C;AACrE,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACJ,UAAA;AACK,UAAA;AACF,UAAA;AACA,UAAA;AACH,UAAA;AACzB,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACA,MAAA;AACJ,QAAA;AACa,QAAA;AACD,QAAA;AACD,QAAA;AACd,QAAA;AACO,UAAA;AACV,UAAA;AACE,UAAA;AACC,UAAA;AACI,UAAA;AACG,UAAA;AACR,UAAA;AACc,UAAA;AAC1B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACe,UAAA;AACC,UAAA;AAC5B,QAAA;AACkB,QAAA;AACI,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACb,QAAA;AACI,QAAA;AACQ,QAAA;AACT,QAAA;AACrB,MAAA;AACF,IAAA;AACF,EAAA;AACF;AxB+sMsC;AACA;AyB7vMH;AACE;AACF;AACC;AAEJ;AACA;AACG;AAgBvB;AACJ,EAAA;AACO,EAAA;AACD,EAAA;AACuB,IAAA;AACL,IAAA;AACD,IAAA;AACO,IAAA;AACL,IAAA;AACF,IAAA;AACM,IAAA;AACJ,IAAA;AACI,IAAA;AACR,IAAA;AAClB,IAAA;AACC,MAAA;AACyB,MAAA;AAClB,MAAA;AACf,IAAA;AACF,EAAA;AACsB,EAAA;AACxB;AAEiB;AACT,EAAA;AACO,EAAA;AACD,EAAA;AACiB,IAAA;AACK,IAAA;AACJ,IAAA;AACK,IAAA;AACF,IAAA;AACC,IAAA;AACP,IAAA;AACK,IAAA;AACC,IAAA;AACE,IAAA;AACN,IAAA;AAC7B,EAAA;AACsB,EAAA;AACxB;AAEc;AACN,EAAA;AACO,EAAA;AACD,EAAA;AACsB,IAAA;AACC,IAAA;AACD,IAAA;AACR,IAAA;AACC,IAAA;AACQ,IAAA;AACnC,EAAA;AACsB,EAAA;AACxB;AAEwC;AAEvB;AACT,EAAA;AAEJ,EAAA;AACU,EAAA;AACgB,IAAA;AACM,IAAA;AACN,IAAA;AAClB,IAAA;AACI,IAAA;AACJ,MAAA;AACO,MAAA;AACD,MAAA;AACoB,QAAA;AACL,QAAA;AACA,QAAA;AACE,QAAA;AACC,QAAA;AAC9B,MAAA;AACsB,MAAA;AACxB,IAAA;AACF,EAAA;AACsB,EAAA;AACxB;AAEoB;AACL,EAAA;AACuB,EAAA;AACtC;AAEgB;AACR,EAAA;AACO,EAAA;AACD,EAAA;AACkB,IAAA;AACA,IAAA;AACpB,IAAA;AACV,EAAA;AACsB,EAAA;AACxB;AAEe;AACP,EAAA;AACO,EAAA;AACD,EAAA;AACkB,IAAA;AACF,IAAA;AACQ,IAAA;AACpC,EAAA;AACsB,EAAA;AACxB;AAEkB;AACV,EAAA;AACO,EAAA;AACD,EAAA;AACJ,IAAA;AACE,MAAA;AACa,MAAA;AAEjB,MAAA;AACJ,IAAA;AACiC,IAAA;AACA,IAAA;AACN,IAAA;AACO,IAAA;AACP,IAAA;AACM,IAAA;AACL,IAAA;AACM,IAAA;AACpC,EAAA;AACsB,EAAA;AACxB;AAEsB;AACd,EAAA;AACO,EAAA;AACD,EAAA;AACJ,IAAA;AACE,MAAA;AACiB,MAAA;AACV,MAAA;AACf,IAAA;AAC6B,IAAA;AACK,IAAA;AACN,IAAA;AACK,IAAA;AACN,IAAA;AACM,IAAA;AACnC,EAAA;AACsB,EAAA;AACxB;AAEuB;AACf,EAAA;AACO,EAAA;AACD,EAAA;AACwB,IAAA;AACD,IAAA;AACP,IAAA;AACO,IAAA;AACP,IAAA;AACG,IAAA;AACF,IAAA;AACO,IAAA;AACP,IAAA;AACM,IAAA;AACL,IAAA;AAC9B,EAAA;AACsB,EAAA;AACxB;AAEsB;AACd,EAAA;AACO,EAAA;AACD,EAAA;AACgB,IAAA;AACG,IAAA;AACK,IAAA;AACL,IAAA;AACI,IAAA;AACP,IAAA;AAC5B,EAAA;AACsB,EAAA;AACxB;AAEsB;AACd,EAAA;AACO,EAAA;AACD,EAAA;AACe,IAAA;AACE,IAAA;AACG,IAAA;AACI,IAAA;AACP,IAAA;AACI,IAAA;AACjC,EAAA;AACsB,EAAA;AACxB;AAEqB;AACgB,EAAA;AACA,EAAA;AAC3B,EAAA;AACA,IAAA;AACuB,IAAA;AAChB,IAAA;AACf,EAAA;AACK,EAAA;AAC6B,EAAA;AACP,EAAA;AACQ,EAAA;AACzB,EAAA;AACH,EAAA;AACwB,EAAA;AACrB,EAAA;AACD,EAAA;AACD,EAAA;AACH,EAAA;AACI,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACF,EAAA;AACT;AAEe;AACP,EAAA;AACI,EAAA;AACA,EAAA;AACG,EAAA;AACN,EAAA;AACC,IAAA;AACM,IAAA;AACW,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACI,MAAA;AACA,MAAA;AACG,MAAA;AACD,MAAA;AACF,MAAA;AAC3B,IAAA;AACmB,IAAA;AACG,IAAA;AACxB,EAAA;AACF;AAEqC;AAC4B,EAAA;AAClC,EAAA;AAC/B;AAEqC;AAC5B,EAAA;AACiB,IAAA;AAChB,IAAA;AACE,IAAA;AACI,IAAA;AACF,IAAA;AACE,IAAA;AACH,IAAA;AACG,IAAA;AACc,IAAA;AACP,IAAA;AACD,IAAA;AACgB,IAAA;AACP,IAAA;AACI,IAAA;AACL,IAAA;AAC5B,EAAA;AACF;AAGgB;AACJ,EAAA;AACkB,EAAA;AACc,EAAA;AAEQ,EAAA;AAClB,IAAA;AACJ,IAAA;AACO,IAAA;AACxB,IAAA;AACX,EAAA;AAII,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWD,IAAAA;AACS,IAAA;AACP,IAAA;AACQ,MAAA;AACT,MAAA;AACL,MAAA;AACW,QAAA;AACC,QAAA;AACH,QAAA;AACW,QAAA;AACR,QAAA;AACU,QAAA;AACV,QAAA;AACR,QAAA;AACS,QAAA;AACS,QAAA;AACnB,QAAA;AACW,QAAA;AACX,QAAA;AACA,QAAA;AACqB,QAAA;AAClB,QAAA;AACa,QAAA;AAC1B,MAAA;AACF,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACC,QAAA;AACJ,QAAA;AACE,QAAA;AAC3B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACN,IAAA;AACiB,MAAA;AACC,MAAA;AAClC,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACC,QAAA;AACJ,QAAA;AACzB,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACQ,IAAA;AAC/B,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWA,IAAAA;AACS,IAAA;AACN,IAAA;AACa,MAAA;AACC,MAAA;AACA,MAAA;AAC9B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACAA,IAAAA;AACS,IAAA;AACS,IAAA;AAChC,EAAA;AAEc,EAAA;AACP,IAAA;AACM,IAAA;AACkB,MAAA;AACD,MAAA;AAC7B,IAAA;AACsB,IAAA;AACxB,EAAA;AAEI,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACP,QAAA;AACM,QAAA;AACA,QAAA;AACA,QAAA;AACE,QAAA;AACD,QAAA;AACC,QAAA;AACT,QAAA;AACA,QAAA;AACY,QAAA;AACR,QAAA;AACO,QAAA;AACD,QAAA;AACJ,QAAA;AAC1B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AAC7B,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWA,IAAAA;AACS,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACc,QAAA;AACM,QAAA;AACH,QAAA;AACC,QAAA;AAC9B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACO,IAAA;AAC9B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACiB,QAAA;AACH,QAAA;AACI,QAAA;AACD,QAAA;AACpB,QAAA;AACC,UAAA;AACqB,UAAA;AACd,UAAA;AACf,QAAA;AAC4B,QAAA;AACrB,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACM,YAAA;AACM,YAAA;AACJ,YAAA;AACM,YAAA;AAC1B,UAAA;AACsB,UAAA;AACxB,QAAA;AACF,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACsB,IAAA;AACY,IAAA;AACnC,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWA,IAAAA;AACS,IAAA;AACN,IAAA;AACiB,MAAA;AACvB,QAAA;AACG,QAAA;AACY,QAAA;AACC,QAAA;AACK,QAAA;AAC3B,MAAA;AACJ,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACI,IAAA;AACA,MAAA;AACC,MAAA;AAC3B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACiB,QAAA;AACD,QAAA;AACG,QAAA;AAC/B,MAAA;AAC6B,MAAA;AACP,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACDC,QAAAA;AACU,QAAA;AACQ,QAAA;AACF,QAAA;AACG,QAAA;AACH,QAAA;AAC3B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWD,IAAAA;AACS,IAAA;AACP,IAAA;AACQ,MAAA;AACW,MAAA;AAClC,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AAC6B,IAAA;AACT,IAAA;AACD,IAAA;AACG,MAAA;AACY,MAAA;AACX,MAAA;AACC,MAAA;AACI,MAAA;AAC9B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AAC6B,IAAA;AACT,IAAA;AACQ,IAAA;AAC/B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACY,QAAA;AACE,QAAA;AAC1B,MAAA;AACe,MAAA;AACO,MAAA;AACxB,IAAA;AACsB,IAAA;AACK,IAAA;AAC5B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACD,IAAA;AACH,MAAA;AACK,MAAA;AACvB,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACqB,MAAA;AACL,MAAA;AACxB,IAAA;AACsB,IAAA;AACE,IAAA;AACzB,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACiB,QAAA;AACC,QAAA;AAC9B,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACsB,IAAA;AACO,IAAA;AAC9B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACY,QAAA;AACI,QAAA;AACC,QAAA;AACR,QAAA;AACrB,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACS,IAAA;AACF,MAAA;AACpB,MAAA;AACI,QAAA;AACgB,UAAA;AACD,UAAA;AACxB,QAAA;AACF,MAAA;AACF,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACoBE,MAAAA;AACJ,MAAA;AACxB,IAAA;AACsB,IAAA;AACY,IAAA;AACL,MAAA;AACO,MAAA;AACb,MAAA;AACI,MAAA;AAC3B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AAC6B,IAAA;AACT,IAAA;AACE,IAAA;AACI,MAAA;AACL,MAAA;AACO,MAAA;AAC9B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACW,QAAA;AACA,QAAA;AACM,QAAA;AAC7B,MAAA;AACmB,MAAA;AACG,MAAA;AACxB,IAAA;AACsB,IAAA;AACY,IAAA;AACnC,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWF,IAAAA;AACS,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACE,IAAA;AACzB,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACW,IAAA;AAClC,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACF,QAAA;AACCC,QAAAA;AACkB,QAAA;AACF,QAAA;AACN,QAAA;AACP,QAAA;AACa,QAAA;AACE,QAAA;AAC7B,MAAA;AACmB,MAAA;AACG,MAAA;AACxB,IAAA;AACsB,IAAA;AACA,IAAA;AACJ,MAAA;AACW,MAAA;AACC,MAAA;AACC,MAAA;AAC/B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACmB,QAAA;AACJ,QAAA;AAC3B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AACD,MAAA;AACO,MAAA;AAC5B,MAAA;AAAgC,QAAA;AAAkH,MAAA;AAC/I,MAAA;AACM,QAAA;AACgB,QAAA;AACA,UAAA;AACA,UAAA;AACD,UAAA;AACjB,UAAA;AACC,YAAA;AACK,YAAA;AACM,YAAA;AACE,YAAA;AACV,YAAA;AACgB,YAAA;AACjB,YAAA;AACU,YAAA;AACM,YAAA;AAC1B,UAAA;AACD,QAAA;AACH,MAAA;AACF,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACkB,QAAA;AACN,QAAA;AACA,QAAA;AACG,QAAA;AACC,QAAA;AACA,QAAA;AAClB,QAAA;AACA,UAAA;AACO,UAAA;AACY,UAAA;AACN,UAAA;AACG,UAAA;AACxB,QAAA;AACuB,QAAA;AACJ,QAAA;AACQ,QAAA;AACA,QAAA;AACA,QAAA;AAC7B,MAAA;AACsB,MAAA;AACA,MAAA;AACxB,IAAA;AACsB,IAAA;AACN,IAAA;AACC,MAAA;AACc,MAAA;AAC/B,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACc,QAAA;AACI,QAAA;AACD,QAAA;AACD,QAAA;AAC5B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACQ,IAAA;AACL,MAAA;AACL,MAAA;AACpB,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACY,IAAA;AACnC,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACAD,IAAAA;AACoBG,IAAAA;AAClB,IAAA;AACS,MAAA;AACD,MAAA;AACQ,MAAA;AAC/B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACAH,IAAAA;AACoBG,IAAAA;AAClB,IAAA;AACS,MAAA;AACD,MAAA;AACQ,MAAA;AAC/B,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACkB,QAAA;AACD,QAAA;AACD,QAAA;AAC5B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACS,IAAA;AACzB,MAAA;AAAqC,QAAA;AAA2F,MAAA;AACrG,MAAA;AACJ,MAAA;AAC7B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACC,QAAA;AACF,QAAA;AACC,QAAA;AAC5B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACI,IAAA;AACF,MAAA;AACO,MAAA;AAC/B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWH,IAAAA;AACS,IAAA;AACP,IAAA;AACS,MAAA;AACS,MAAA;AACjC,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACK,IAAA;AACG,MAAA;AAChB,MAAA;AACoB,MAAA;AAClC,IAAA;AACD,EAAA;AAEmC,EAAA;AACA,EAAA;AACA,EAAA;AACtB,EAAA;AACA,EAAA;AACoB,EAAA;AAEP,EAAA;AAC7B;AzB8oMsC;AACA;A0BtrOmD;AACnD,EAAA;AACT,EAAA;AACS,EAAA;AACb,EAAA;AACzB;AAY0B;AAatB,EAAA;AAFiB,IAAA;AACA,IAAA;AAChB,EAAA;AAFgB,EAAA;AACA,EAAA;AAXD,oBAAA;AAAA;AAEK,oBAAA;AAAA;AAEE,oBAAA;AACJ,oBAAA;AACU,oBAAA;AACb,oBAAA;AAO2C,EAAA;AACxC,IAAA;AACO,IAAA;AACQ,IAAA;AACpC,EAAA;AAE4C,EAAA;AACtB,IAAA;AACa,IAAA;AACnC,EAAA;AAAA;AAGoC,EAAA;AACrB,IAAA;AACS,IAAA;AACO,IAAA;AACZ,MAAA;AACiB,MAAA;AACL,IAAA;AACpB,MAAA;AACT,IAAA;AAC4B,IAAA;AACE,IAAA;AACV,IAAA;AACS,MAAA;AACpB,MAAA;AACT,IAAA;AACuB,IAAA;AACF,MAAA;AACW,MAAA;AACvB,MAAA;AACT,IAAA;AAC+B,IAAA;AAClB,MAAA;AACJ,MAAA;AACT,IAAA;AACI,IAAA;AACkB,MAAA;AACY,MAAA;AAChB,MAAA;AACc,MAAA;AACvB,MAAA;AACK,IAAA;AACgB,MAAA;AACrB,MAAA;AACT,IAAA;AACF,EAAA;AAEyC,EAAA;AACnB,IAAA;AACW,IAAA;AACE,IAAA;AACH,IAAA;AAChC,EAAA;AAEiC,EAAA;AACG,IAAA;AACnB,IAAA;AACa,IAAA;AACN,IAAA;AACW,MAAA;AAC/B,MAAA;AACF,IAAA;AACyB,IAAA;AACJ,MAAA;AACI,MAAA;AACM,MAAA;AACE,MAAA;AACL,MAAA;AAC1B,MAAA;AACF,IAAA;AACgC,IAAA;AAClB,IAAA;AACU,IAAA;AACM,IAAA;AACH,IAAA;AACK,IAAA;AAClC,EAAA;AAE+B,EAAA;AACK,IAAA;AACnB,IAAA;AACa,IAAA;AAChB,IAAA;AACM,IAAA;AACY,IAAA;AAChC,EAAA;AAEe,EAAA;AACE,IAAA;AACG,IAAA;AACgB,IAAA;AACtB,IAAA;AACd,EAAA;AAEsB,EAAA;AACC,IAAA;AACvB,EAAA;AAAA;AAGyF,EAAA;AAC7D,IAAA;AACV,IAAA;AACY,MAAA;AAC1B,MAAA;AACF,IAAA;AACmB,IAAA;AACa,IAAA;AACR,IAAA;AAEU,IAAA;AACD,IAAA;AACD,IAAA;AAClC,EAAA;AACF;AAIE;AAI2B,EAAA;AACG,EAAA;AACN,EAAA;AACI,IAAA;AACM,IAAA;AACX,IAAA;AACd,IAAA;AACT,EAAA;AACwB,EAAA;AACN,IAAA;AACT,IAAA;AACT,EAAA;AACgB,EAAA;AACe,EAAA;AACrB,EAAA;AACY,IAAA;AACb,IAAA;AACT,EAAA;AACoC,EAAA;AACJ,EAAA;AACA,IAAA;AACD,IAAA;AACtB,IAAA;AACT,EAAA;AACiB,EAAA;AACO,IAAA;AACf,IAAA;AACT,EAAA;AACiB,EAAA;AACe,IAAA;AACvB,IAAA;AACT,EAAA;AACsB,EAAA;AACC,IAAA;AACd,IAAA;AACT,EAAA;AACO,EAAA;AACT;AASkC;AAKgC,EAAA;AAAnC,IAAA;AAAoC,EAAA;AAApC,EAAA;AAJkC,oBAAA;AACT,oBAAA;AACvC,oBAAA;AAAA;AAKqB,EAAA;AACH,IAAA;AACA,IAAA;AACH,IAAA;AACJ,IAAA;AACV,MAAA;AACQ,MAAA;AACZ,MAAA;AACV,IAAA;AACY,IAAA;AACkC,IAAA;AACf,IAAA;AACN,MAAA;AACQ,MAAA;AACJ,MAAA;AACH,MAAA;AACE,MAAA;AACV,MAAA;AACA,MAAA;AACwB,MAAA;AACxC,MAAA;AACU,IAAA;AACL,IAAA;AACT,EAAA;AACF;AAG4J;AAC5H,EAAA;AACN,EAAA;AACiF,EAAA;AACrE,EAAA;AACjB,IAAA;AACa,IAAA;AACD,IAAA;AAC/B,EAAA;AACyB,EAAA;AACS,EAAA;AAC3B,EAAA;AACT;AAWuD;AAClB,EAAA;AACL,EAAA;AACK,EAAA;AACJ,EAAA;AACD,EAAA;AACE,EAAA;AACJ,EAAA;AACK,EAAA;AACD,EAAA;AACI,EAAA;AACD,EAAA;AACC,EAAA;AACH,EAAA;AACnC;AAEuD;AAC7B,EAAA;AACA,EAAA;AACE,EAAA;AACK,EAAA;AACF,EAAA;AACE,EAAA;AACH,EAAA;AACD,EAAA;AAC7B;AAG6D;AAC1C,EAAA;AACgB,EAAA;AACE,EAAA;AACrC;AAG8G;AACzE,EAAA;AACA,EAAA;AAC8B,EAAA;AACnE;AAGkE;AAC3B,EAAA;AACH,EAAA;AACpC;AAGyD;AACpB,EAAA;AACrC;AAM+C;AACV,EAAA;AAClB,EAAA;AACc,EAAA;AACE,IAAA;AACR,MAAA;AACA,MAAA;AACU,MAAA;AACP,MAAA;AACM,MAAA;AACH,MAAA;AAEG,MAAA;AACpB,MAAA;AACI,MAAA;AACA,MAAA;AACiB,MAAA;AACjC,IAAA;AACF,EAAA;AACoC,EAAA;AACtC;AAGqE;AACnC,EAAA;AACC,EAAA;AACH,EAAA;AACC,EAAA;AACjC;AAa4D;AAC9B,EAAA;AACH,EAAA;AAC3B;AAyBiD;AACb,EAAA;AACH,EAAA;AACjC;AASiI;AACzG,EAAA;AACf,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACmB,UAAA;AACD,UAAA;AACE,UAAA;AACnB,UAAA;AACoB,UAAA;AACF,UAAA;AACE,UAAA;AACH,UAAA;AACjB,UAAA;AACV,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACS,QAAA;AACjB,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACkB,UAAA;AACjB,UAAA;AACiB,UAAA;AACjB,UAAA;AACgB,UAAA;AACE,UAAA;AAC5B,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACR,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACoB,UAAA;AACnB,UAAA;AACA,UAAA;AACmB,UAAA;AACnB,UAAA;AACV,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACS,QAAA;AACjB,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACC,UAAA;AACiB,UAAA;AACG,UAAA;AACJ,UAAA;AACG,UAAA;AACH,UAAA;AACrB,UAAA;AACL,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACO,QAAA;AACf,MAAA;AACU,IAAA;AACsB,MAAA;AAErB,MAAA;AAEyB,QAAA;AACE,QAAA;AACH,QAAA;AACC,QAAA;AACD,QAAA;AACG,QAAA;AAC3B,QAAA;AAGH,MAAA;AAC8B,QAAA;AACA,QAAA;AAEL,MAAA;AACJ,MAAA;AAC/B,IAAA;AACK,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACqB,UAAA;AACA,UAAA;AAC9B,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACR,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACoB,UAAA;AACF,UAAA;AAC3B,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACR,MAAA;AACG,IAAA;AAC4B,MAAA;AAC5B,IAAA;AACa,MAAA;AAClB,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAG0E;AAC5C,EAAA;AACQ,EAAA;AACH,EAAA;AACnC;AAGiE;AACwB,EAAA;AAC7D,IAAA;AACM,IAAA;AACII,IAAAA;AACpC,EAAA;AACmC,EAAA;AACA,EAAA;AAC0D,EAAA;AAC/F;AAG+D;AAC/B,EAAA;AACM,EAAA;AACtC;AAGkE;AAGZ;AACrB,EAAA;AACjC;AAOkE;AACzD,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACC,EAAA;AACC,EAAA;AACF,EAAA;AACR;AAG2D;AAC7B,EAAA;AACE,EAAA;AACE,EAAA;AACF,EAAA;AACI,EAAA;AACV,EAAA;AACQ,EAAA;AACI,EAAA;AACV,EAAA;AAC5B;AAG0D;AACxB,EAAA;AACJ,EAAA;AAC9B;AAGuD;AAGyB;AACtD,EAAA;AACU,EAAA;AACpC;AAG2D;AACvB,EAAA;AACjB,EAAA;AACV,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACwB,MAAA;AACxB,IAAA;AACU,MAAA;AACf,IAAA;AACqB,MAAA;AACvB,EAAA;AACF;AAOiE;AACrD,EAAA;AACa,EAAA;AACC,EAAA;AACD,EAAA;AAAC,EAAA;AACV,EAAA;AACL,EAAA;AACmB,EAAA;AACb,IAAA;AACqB,IAAA;AAAE,IAAA;AACvB,IAAA;AACf,EAAA;AACa,EAAA;AACA,EAAA;AACD,IAAA;AACF,oBAAA;AACV,EAAA;AACF;AAMoE;AACxC,EAAA;AACO,EAAA;AACF,EAAA;AACN,EAAA;AACC,EAAA;AACM,EAAA;AACT,EAAA;AACF,IAAA;AACmB,IAAA;AACxC,EAAA;AACa,EAAA;AACf;AAGwF;AAGL;AAG5B;AAC3B,EAAA;AACE,EAAA;AACO,EAAA;AACrC;AAGsF;AACC;AAGR;AAGU;AACrD,EAAA;AACA,EAAA;AACpC;AAGyD;AACnB,EAAA;AACT,EAAA;AACD,EAAA;AAC5B;AAG+I;AAC7G,EAAA;AAClB,EAAA;AACP,IAAA;AAC6B,MAAA;AAC7B,IAAA;AAC6B,MAAA;AAC7B,IAAA;AAC4B,MAAA;AAC5B,IAAA;AAC4B,MAAA;AACnC,EAAA;AACF;A1BshOsC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/paint-editor/index.cjs","sourcesContent":[null,"/**\n * The paint editor's document: a canvas (size and resolution), a paper, and the shared raster\n * layer stack (`@pixerate/editor/raster`: pixel layers and groups with masks, clipping, locks and\n * blend modes). Layers are immutable and share tiles, so a history entry is a reference to a\n * whole document state.\n *\n * Saved documents are JSON plus PNG assets, through the image editor's layer serializer (so the\n * same layer tree loads in both editors), with the paper as plain settings.\n */\n\nimport type { RasterGroupLayer, RasterLayer, RasterPixelLayer } from '../raster/layers';\nimport { emptySurface } from '../raster/surface';\nimport { normalizePaper, type PaperSettings } from './paper';\nimport type { RecordedStroke } from './strokes';\nimport type { WetEvent } from './wet';\nimport type { ThickChannels } from './thick';\nimport type { Sticker } from './stickers';\n\nexport type PaintPixelLayer = RasterPixelLayer;\nexport type PaintGroupLayer = RasterGroupLayer<PaintLayer>;\n/** A layer of the paint editor: the shared stack's pixel layers and groups. */\nexport type PaintLayer = RasterLayer;\n\n/** Canvas size limits (pixels per side). */\nexport const MIN_CANVAS_SIDE = 1;\nexport const MAX_CANVAS_SIDE = 16384;\nexport const DEFAULT_DPI = 300;\n\nexport interface PaintCanvas {\n width: number;\n height: number;\n /** Pixels per inch, for print sizes and PSD resolution (default 300). */\n dpi: number;\n}\n\n/** Everything one undo step restores. */\nexport interface PaintDocumentState {\n canvas: PaintCanvas;\n paper: PaperSettings;\n /** Bottom to top. */\n layers: PaintLayer[];\n activeLayerId: string | null;\n /** Every committed stroke, oldest first (strokes are data: replay, vary, time-lapse later). */\n strokes: RecordedStroke[];\n /** Wet-media events other than strokes (drying, tilt, settings), with the clock they happened at. */\n wetEvents?: WetEvent[];\n /** Changes besides strokes that a replay repeats (fills, clears, flattened relief), in order. */\n paintEvents?: PaintEvent[];\n /** Sticker layers: each layer's stickers, bottom first (its pixels are drawn from them). */\n stickers?: Readonly<Record<string, Sticker[]>>;\n /** Thick paint: each layer's height and material channels (layers without relief are missing). */\n thick?: Readonly<Record<string, ThickChannels>>;\n}\n\n/** A change a replay repeats besides strokes, recorded with how many strokes came before it. */\nexport interface PaintEvent {\n at: number;\n kind: 'fill' | 'clear' | 'flatten' | 'stickers';\n layerId: string;\n /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers after an edit. */\n data?: Record<string, unknown>;\n}\n\nlet nextId = 1;\n\n/** A new layer id, unique in this session. */\nexport function newPaintLayerId(): string {\n return `pl${Date.now().toString(36)}${(nextId++).toString(36)}`;\n}\n\n/** An empty, visible, normal pixel layer. */\nexport function createPaintLayer(name: string, id = newPaintLayerId()): PaintPixelLayer {\n return { type: 'pixel', id, name, visible: true, opacity: 1, blend: 'normal', surface: emptySurface(4), offset: { x: 0, y: 0 } };\n}\n\nexport class PaintDocumentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PaintDocumentError';\n }\n}\n\n/** Validates and rounds a canvas size; throws a `PaintDocumentError` that says what is wrong. */\nexport function normalizeCanvas(raw: Partial<PaintCanvas>): PaintCanvas {\n const side = (v: unknown, what: string) => {\n if (typeof v !== 'number' || !Number.isFinite(v)) throw new PaintDocumentError(`${what} must be a number of pixels (${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE}).`);\n const n = Math.round(v);\n if (n < MIN_CANVAS_SIDE || n > MAX_CANVAS_SIDE) throw new PaintDocumentError(`${what} ${n} is out of range; use ${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE} pixels.`);\n return n;\n };\n const dpi = raw.dpi === undefined ? DEFAULT_DPI : raw.dpi;\n if (typeof dpi !== 'number' || !Number.isFinite(dpi) || dpi < 1 || dpi > 10000) throw new PaintDocumentError(`dpi ${dpi} is out of range; use 1..10000.`);\n return { width: side(raw.width, 'width'), height: side(raw.height, 'height'), dpi: Math.round(dpi * 100) / 100 };\n}\n\n/** A blank document: one empty layer over the paper. */\nexport function createPaintDocument(options: Partial<PaintCanvas> & { paper?: Partial<PaperSettings> } = {}): PaintDocumentState {\n const canvas = normalizeCanvas({ width: options.width ?? 2048, height: options.height ?? 1536, dpi: options.dpi });\n const layer = createPaintLayer('Layer 1');\n return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };\n}\n","/**\n * The paint editor's dab-by-dab stroke engine: every brush whose dabs read what earlier dabs laid\n * down. The Mixer Brush, the blender, the smudge, and dab brushes that pick up paint or are\n * multi-coloured all run here, with the same pen input as dry media (`PenDabWalker`: pressure,\n * tilt, barrel rotation, velocity, rotation modes, jitter), paper grain and symmetry copies.\n *\n * - `mixer` and wet-capable `dab` brushes go through the shared pick-up / mix exchange\n * (`PaintExchange` over a `TileMedium`): each dab picks up from the layer, mixes its paint from\n * the brush colour (or the multi-colour gradient) and the carried paint, and lays it down. A\n * Mixer Brush deposits per dab (photocraft's model); a dab brush keeps the stroke's opacity\n * ceiling: a shared `CoverageMap` builds up as for dry media and each dab lays only the\n * increase, `k = (c₁ − c₀)·o / (1 − c₀·o)`, so a one-colour Normal stroke matches the dry engine.\n * - `blender` calls `blenderDab`, `smudge` a `SmudgeTip` per copy.\n *\n * Pixels go through the session's live tiles (`PaintTileTarget`). Pure data in, pixels out:\n * points, seed, colours and the starting load replay exactly.\n */\n\nimport { CoverageMap, dabRect, rasterizeDab } from '../paint/brush';\nimport { blenderDab, createBlenderScratch, SmudgeTip, type BlenderScratch } from '../paint/blending';\nimport { exchangeRates, PaintExchange, TileMedium, type MixerLoad, type MixingModel, type PaintTileTarget, type RgbTriple } from '../paint/exchange';\nimport { mixerRates } from '../paint/mixer';\nimport { mixPigment8 } from '../raster/blend';\nimport type { RasterRect } from '../raster/surface';\nimport { PaintGradientSampler, type PaintGradientSample, type PaintGradientSource } from '../paint/gradient-paint';\nimport type { MultiColorSettings, PaintBrush } from './brushes';\nimport { IDENTITY_COPY, PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine';\nimport { rand01 } from './noise';\nimport { grainCatch, type PaperGrain } from './paper';\n\nexport interface PaintDabStrokeOptions {\n /** Where the stroke reads and writes pixels. */\n target: PaintTileTarget;\n /** The paint colour, sRGB 0..1. */\n color: RgbTriple;\n /** The secondary colour (a multi-colour brush without its own colours uses both). */\n secondary: RgbTriple;\n /** What the brush carries at the start (mixer and wet-capable dab brushes). */\n load: MixerLoad;\n grain: PaperGrain | null;\n paperDepth: number;\n seed?: number;\n copies?: readonly StrokeCopy[];\n /** Gradient paint (resolved: no `paint` / `secondary` stops): each dab's colour comes from it. */\n paint?: PaintGradientSource | null;\n}\n\n/** Entries in a multi-colour gradient table. */\nconst GRADIENT_STEPS = 32;\n\n/** Colours of a multi-colour brush, resolved against the paint and secondary colours. */\nexport function multiColorPalette(mc: MultiColorSettings, color: RgbTriple, secondary: RgbTriple): RgbTriple[] {\n if (mc.colors.length >= 2) {\n return mc.colors.map((h) => {\n const n = parseInt(h.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n });\n }\n return [[...color], [...secondary]];\n}\n\n/** The blend position `t` between two neighbouring colours at fraction `f`, by softness (0: a hard step at ½). */\nfunction soften(f: number, softness: number): number {\n if (softness >= 1) return f;\n const w = Math.max(1e-3, softness);\n return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));\n}\n\n/** `a` mixed `t` of the way to `b` by the mixing model (8-bit pigment spectra cached). */\nfunction mixRgb(a: RgbTriple, b: RgbTriple, t: number, mixing: MixingModel): RgbTriple {\n if (t <= 0) return [...a];\n if (t >= 1) return [...b];\n if (mixing === 'pigment') {\n const o = [0, 0, 0];\n const q = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255);\n mixPigment8(q(a[0]), q(a[1]), q(a[2]), q(b[0]), q(b[1]), q(b[2]), t, o);\n return [o[0], o[1], o[2]];\n }\n return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n}\n\n/** The tip gradient of a multi-colour brush: `GRADIENT_STEPS` colours across the tip. */\nexport function multiColorGradient(palette: readonly RgbTriple[], softness: number, mixing: MixingModel): RgbTriple[] {\n const n = palette.length;\n const out: RgbTriple[] = [];\n for (let j = 0; j < GRADIENT_STEPS; j++) {\n // Each colour owns an equal band; blends happen across band edges.\n const f = (j / (GRADIENT_STEPS - 1)) * (n - 1);\n const i = Math.min(n - 2, Math.floor(f));\n out.push(mixRgb(palette[i], palette[i + 1], soften(f - i, softness), mixing));\n }\n return out;\n}\n\n/** The colour of dab `index` of a `dab`-mode multi-colour brush. */\nexport function multiColorForDab(mc: MultiColorSettings, palette: readonly RgbTriple[], index: number, seed: number, mixing: MixingModel): RgbTriple {\n const n = palette.length;\n if (mc.order === 'random') {\n const run = Math.floor(index / mc.length);\n return [...palette[Math.min(n - 1, Math.floor(rand01(seed, run, 11) * n))]];\n }\n const f = (index / mc.length) % n;\n const i = Math.floor(f);\n return mixRgb(palette[i], palette[(i + 1) % n], soften(f - i, mc.softness), mixing);\n}\n\ninterface CopyState {\n walker: PenDabWalker;\n exchange: PaintExchange | null;\n smudge: SmudgeTip | null;\n}\n\nexport class PaintDabStroke {\n dabCount = 0;\n private readonly smoother: PenSmoother;\n private readonly copies: readonly StrokeCopy[];\n private readonly states: CopyState[];\n private readonly medium: TileMedium;\n private readonly depth: number;\n private readonly coverage: CoverageMap | null;\n private readonly sampler: PaintGradientSampler | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n private readonly gradient: RgbTriple[] | null;\n /** Opacity across the tip (`across-tip` gradients with opacity stops), or `null`. */\n private readonly gradientAlpha: number[] | null;\n private alphaCov = new Float32Array(0);\n private readonly fieldLut: RgbTriple[] = [];\n private readonly scratch: BlenderScratch | null;\n private readonly mixing: MixingModel;\n private tip = new Float32Array(0);\n private cov = new Float32Array(0);\n private field = new Float32Array(0);\n private readonly paint: RgbTriple = [0, 0, 0];\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n\n constructor(\n readonly brush: PaintBrush,\n private readonly options: PaintDabStrokeOptions\n ) {\n this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);\n this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];\n const exchanging = brush.engine === 'mixer' || brush.engine === 'dab';\n const rates = brush.engine === 'mixer' ? mixerRates(brush.mixer) : exchangeRates(brush.behavior.pickup, brush.behavior.mix, 1, brush.behavior.mixing);\n this.mixing = brush.engine === 'mixer' ? brush.mixer.mixing : brush.behavior.mixing;\n this.states = this.copies.map((c, i) => ({\n walker: new PenDabWalker(brush, ((options.seed ?? 0) + i * 7919) | 0, c.linear),\n exchange: exchanging ? new PaintExchange(rates, options.load) : null,\n smudge: brush.engine === 'smudge' ? new SmudgeTip(brush.smudge, brush.tip.size * (1 + brush.dynamics.tiltSize * 1.5)) : null,\n }));\n // A Mixer Brush mixes as its settings say; a dab brush blends by its tip's blend mode.\n this.medium = new TileMedium(options.target, brush.engine === 'dab' ? { blend: brush.tip.blend } : {});\n this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;\n this.coverage = brush.engine === 'dab' ? new CoverageMap() : null;\n this.sampler = exchanging && options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;\n const table = this.sampler?.acrossTip ? this.sampler.tipTable(GRADIENT_STEPS) : null;\n this.gradient = table ? table.map((e) => e.rgb) : null;\n this.gradientAlpha = table && table.some((e) => e.alpha < 1) ? table.map((e) => e.alpha) : null;\n this.scratch = brush.engine === 'blender' ? createBlenderScratch() : null;\n }\n\n /** What copy 0's brush carries after the dabs so far (mixer and wet-capable dab brushes). */\n get load(): MixerLoad | null {\n return this.states[0].exchange?.load ?? null;\n }\n\n push(points: readonly PenPoint[]): RasterRect[] {\n const rects: RasterRect[] = [];\n for (const raw of points) {\n this.lastRaw = raw;\n const p = this.smoother.push(raw);\n this.lastFed = p;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n this.states[i].walker.push(c.map(p), dabs);\n this.paintDabs(i, dabs, rects);\n });\n }\n return rects;\n }\n\n /** Ends the stroke: the smoothed path catches up with the last raw sample; a tap paints one dab. */\n finish(): RasterRect[] {\n const rects: RasterRect[] = [];\n const raw = this.lastRaw;\n const fed = this.lastFed;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);\n this.states[i].walker.finish(dabs);\n this.paintDabs(i, dabs, rects);\n });\n return rects;\n }\n\n private paintDabs(copy: number, dabs: PenDab[], rects: RasterRect[]): void {\n for (const d of dabs) {\n const rect = dabRect(d, this.brush.tip.hardness);\n if (rect.width <= 0 || rect.height <= 0) continue;\n this.dab(copy, d, rect);\n rects.push(rect);\n this.dabCount++;\n }\n }\n\n /** The tip's pure coverage (`tip`) and, with grain, what the paper catches of it (`cov`). */\n private rasterize(d: PenDab, rect: RasterRect): { tip: Float32Array; cov: Float32Array } {\n const n = rect.width * rect.height;\n if (this.tip.length < n) {\n this.tip = new Float32Array(n);\n this.cov = new Float32Array(n);\n }\n const tip = this.tip.subarray(0, n);\n tip.fill(0);\n rasterizeDab({ ...d, alpha: 1 }, this.brush.tip.hardness, rect, tip);\n if (!(this.depth > 0 && this.options.grain)) return { tip, cov: tip };\n const cov = this.cov.subarray(0, n);\n const grain = this.options.grain;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const y = rect.y + yy + 0.5;\n for (let xx = 0; xx < rect.width; xx++, k++) cov[k] = tip[k] > 0 ? tip[k] * grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure) : 0;\n }\n return { tip, cov };\n }\n\n private dab(copy: number, d: PenDab, rect: RasterRect): void {\n const brush = this.brush;\n const { tip, cov } = this.rasterize(d, rect);\n const st = this.states[copy];\n if (brush.engine === 'blender') {\n blenderDab(this.medium, rect, cov, d.opacity * d.alpha * brush.tip.opacity, d.radius, brush.blender, this.scratch!);\n return;\n }\n if (brush.engine === 'smudge') {\n st.smudge!.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: d.opacity * d.alpha * brush.tip.opacity }, rect, cov);\n return;\n }\n const ex = st.exchange!;\n // 1. Pick up from under the tip (the pure footprint, as the Mixer Brush samples).\n ex.pickUp(this.medium.sample(rect, tip));\n // 2. The dab's paint: one colour, gradient paint's colour for this dab, or a colour per pixel across the tip.\n let flow = 1;\n if (this.sampler && !this.gradient) {\n const g = this.sampler.dab(d, this.sample);\n ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);\n flow = g.alpha;\n }\n let field: Float32Array | null = null;\n let total: number;\n let laid = cov;\n if (this.gradient) {\n total = this.tipField(ex, d, rect);\n field = this.field;\n if (this.gradientAlpha) laid = this.tipAlpha(d, rect, cov);\n } else total = ex.paint(this.paint);\n if (total > 0 && flow > 0) {\n if (this.coverage) this.depositWithCeiling(d, rect, laid, field, ex, flow);\n else {\n const dep = Math.min(1, total) * d.alpha * brush.tip.opacity * d.opacity * flow;\n if (dep > 0) this.medium.deposit(rect, laid, dep, this.paint, ex.paintPigment, field);\n }\n }\n // 3. Drain.\n ex.drain();\n }\n\n /** Fills `this.field` with the tip gradient (mixed with the carried paint) per pixel; returns the paint weight. */\n private tipField(ex: PaintExchange, d: PenDab, rect: RasterRect): number {\n const g = this.gradient!;\n const lut = this.fieldLut;\n let total = 0;\n for (let j = 0; j < g.length; j++) {\n ex.setReservoirColor(g[j][0], g[j][1], g[j][2]);\n const out: RgbTriple = lut[j] ?? [0, 0, 0];\n total = ex.paint(out);\n lut[j] = out;\n }\n const n = rect.width * rect.height;\n if (this.field.length < n * 3) this.field = new Float32Array(n * 3);\n const f = this.field;\n // Across the stroke: the normal to the direction of travel (document y points down).\n const nx = Math.sin(d.direction);\n const ny = Math.cos(d.direction);\n const r = Math.max(0.5, d.radius);\n const last = g.length - 1;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const py = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < rect.width; xx++, k++) {\n const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;\n const j = Math.min(last, Math.max(0, Math.round(((u + 1) / 2) * last)));\n f[k * 3] = lut[j][0];\n f[k * 3 + 1] = lut[j][1];\n f[k * 3 + 2] = lut[j][2];\n }\n }\n return total;\n }\n\n /** `cov` scaled by the gradient's opacity across the tip. */\n private tipAlpha(d: PenDab, rect: RasterRect, cov: Float32Array): Float32Array {\n const a = this.gradientAlpha!;\n const n = rect.width * rect.height;\n if (this.alphaCov.length < n) this.alphaCov = new Float32Array(n);\n const out = this.alphaCov.subarray(0, n);\n const nx = Math.sin(d.direction);\n const ny = Math.cos(d.direction);\n const r = Math.max(0.5, d.radius);\n const last = a.length - 1;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const py = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < rect.width; xx++, k++) {\n const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;\n out[k] = cov[k] * a[Math.min(last, Math.max(0, Math.round(((u + 1) / 2) * last)))];\n }\n }\n return out;\n }\n\n /** A dab brush's deposit: only what this dab adds to the stroke's coverage, under its opacity ceiling. */\n private depositWithCeiling(d: PenDab, rect: RasterRect, cov: Float32Array, field: Float32Array | null, ex: PaintExchange, flow = 1): void {\n const map = this.coverage!;\n const n = rect.width * rect.height;\n const before = new Float32Array(n);\n const vals = new Float32Array(n);\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, k++) {\n if (cov[k] <= 0) continue;\n before[k] = map.get(rect.x + xx, rect.y + yy);\n vals[k] = cov[k] * d.alpha * flow;\n }\n }\n map.accumulate(rect, vals, d.opacity);\n const o = this.brush.tip.opacity;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, k++) {\n if (cov[k] <= 0) {\n vals[k] = 0;\n continue;\n }\n const c0 = before[k] * o;\n const c1 = map.get(rect.x + xx, rect.y + yy) * o;\n vals[k] = c1 > c0 && c0 < 1 ? (c1 - c0) / (1 - c0) : 0;\n }\n }\n this.medium.deposit(rect, vals, 1, this.paint, ex.paintPigment, field);\n }\n}\n","/**\n * Thick paint (impasto): a pixel layer can carry two extra channels, a **height** map (how far the\n * paint stands up, 0..255) and a **material** map (R gloss, G metallic, 0..255), and it is lit at\n * render time: a normal from the height's slope, diffuse light from `PaintLighting`, and a\n * Blinn-Phong highlight whose sharpness follows the gloss, tinted by the paint where it is\n * metallic. The layer's own pixels stay the paint's colour (albedo), so relighting never repaints\n * anything, saving keeps colour, height and material apart, and only tiles whose inputs changed are\n * relit (the lit tiles are cached by the identity of their input tiles).\n *\n * Impasto strokes (`ThickStrokeDriver`) lay colour through the shared pick-up / mix exchange (paint\n * mixes into the wet paint it meets, like oil) and raise the height: `brush` with bristle ridges\n * along the stroke, `tube` in domed blobs, `knife` scraping the relief flat while it smears the\n * paint, `roller` as a flat stippled band, `gloop` as a thick glossy round bead. The paint runs out\n * along the stroke with `load` below 1, and `thinner` flattens and thins it.\n */\n\nimport { dabRect, rasterizeDab } from '../paint/brush';\nimport { SmudgeTip } from '../paint/blending';\nimport { exchangeRates, PaintExchange, TileMedium, type MixerLoad, type PaintTileTarget, type RgbTriple } from '../paint/exchange';\nimport { SRGB_TO_LINEAR, linearToSrgb8 } from '../paint/wet/film';\nimport { PaintGradientSampler, type PaintGradientSample, type PaintGradientSource } from '../paint/gradient-paint';\nimport { TILE, TiledSurface, finishTile, readRegion, surfaceFromPixels, tileKey, type RasterRect, type Tile, type TileKey } from '../raster/surface';\nimport { decodePng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport type { PaintBrush } from './brushes';\nimport { IDENTITY_COPY, PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine';\nimport { hash2 } from './noise';\n\n/** Where the light comes from. */\nexport interface PaintLighting {\n /** Degrees clockwise from the top of the canvas (315: top left, the default). */\n azimuth: number;\n /** Degrees above the canvas, 5 (grazing, deep shadows) .. 90 (straight on, no relief). Default 40. */\n elevation: number;\n /** How strongly the relief shows, 0 (flat) .. 1 (default 0.7). */\n relief: number;\n}\n\nexport const DEFAULT_LIGHTING: Readonly<PaintLighting> = { azimuth: 315, elevation: 40, relief: 0.7 };\n\nexport function normalizeLighting(raw: Partial<PaintLighting> = {}, base: PaintLighting = DEFAULT_LIGHTING): PaintLighting {\n const num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n return {\n azimuth: ((num(raw.azimuth, base.azimuth) % 360) + 360) % 360,\n elevation: Math.min(90, Math.max(5, num(raw.elevation, base.elevation))),\n relief: Math.min(1, Math.max(0, num(raw.relief, base.relief))),\n };\n}\n\n/** A layer's thick-paint channels (layer space, like its pixels). */\nexport interface ThickChannels {\n /** Height 0..255 per pixel (1 channel). */\n height: TiledSurface;\n /** Material per pixel (4 channels): R gloss, G metallic, B and A unused. */\n material: TiledSurface;\n}\n\nexport function emptyThickChannels(): ThickChannels {\n return { height: new TiledSurface(1, new Map(), 0), material: new TiledSurface(4, new Map(), 0) };\n}\n\n/** Height in pixels of the full 0..255 range, for the slopes. */\nconst RELIEF_PX = 7;\nconst AMBIENT = 0.38;\n\n/** Light vector (towards the light) for a lighting. */\nfunction lightVector(l: PaintLighting): [number, number, number] {\n const az = (l.azimuth * Math.PI) / 180;\n const el = (l.elevation * Math.PI) / 180;\n return [Math.cos(el) * Math.sin(az), -Math.cos(el) * Math.cos(az), Math.sin(el)];\n}\n\nexport function lightingKey(l: PaintLighting): string {\n return `${l.azimuth.toFixed(2)}|${l.elevation.toFixed(2)}|${l.relief.toFixed(3)}`;\n}\n\n/**\n * One tile of a thick layer lit: `color` (RGBA, straight), the height and material of the tile, and\n * the height of its 8 neighbours for the slopes at the border (`heightAt(dx, dy)` returns the\n * neighbouring tile's data or null). Returns a new RGBA tile.\n */\nexport function lightTile(color: Uint8ClampedArray, height: Uint8ClampedArray | null, heightAround: (dx: number, dy: number) => Uint8ClampedArray | null, material: Uint8ClampedArray | null, light: PaintLighting): Uint8ClampedArray {\n const out = new Uint8ClampedArray(color);\n if (!height || light.relief <= 0) return out;\n const [lx, ly, lz] = lightVector(light);\n const hv = [lx, ly, lz + 1];\n const hl = Math.hypot(hv[0], hv[1], hv[2]);\n const hx = hv[0] / hl;\n const hy = hv[1] / hl;\n const hz = hv[2] / hl;\n const k = (RELIEF_PX / 255) * light.relief * 2;\n const flatDiffuse = AMBIENT + (1 - AMBIENT) * lz;\n const read = (x: number, y: number): number => {\n if (x >= 0 && y >= 0 && x < TILE && y < TILE) return height[y * TILE + x];\n const dx = x < 0 ? -1 : x >= TILE ? 1 : 0;\n const dy = y < 0 ? -1 : y >= TILE ? 1 : 0;\n const t = heightAround(dx, dy);\n if (!t) return 0;\n return t[(y - dy * TILE) * TILE + (x - dx * TILE)];\n };\n for (let y = 0; y < TILE; y++) {\n for (let x = 0; x < TILE; x++) {\n const p = y * TILE + x;\n const i = p * 4;\n if (color[i + 3] === 0) continue;\n const h = height[p];\n const gx = (read(x + 1, y) - read(x - 1, y)) * 0.5;\n const gy = (read(x, y + 1) - read(x, y - 1)) * 0.5;\n if (h === 0 && gx === 0 && gy === 0) continue;\n let nx = -gx * k;\n let ny = -gy * k;\n const nl = Math.hypot(nx, ny, 1);\n nx /= nl;\n ny /= nl;\n const nz = 1 / nl;\n const diffuse = Math.max(0, nx * lx + ny * ly + nz * lz);\n const shade = (AMBIENT + (1 - AMBIENT) * diffuse) / flatDiffuse;\n const gloss = material ? material[i] / 255 : 0.3;\n const metal = material ? material[i + 1] / 255 : 0;\n const shininess = 6 + 90 * gloss;\n const nh = Math.max(0, nx * hx + ny * hy + nz * hz);\n // Only slopes catch a highlight: subtract what a flat surface would show.\n const spec = Math.max(0, Math.pow(nh, shininess) - Math.pow(hz, shininess)) * (0.25 + 0.75 * gloss) * light.relief;\n for (let c = 0; c < 3; c++) {\n const a = SRGB_TO_LINEAR[color[i + c]];\n const tint = metal * a + (1 - metal);\n const v = a * shade * (1 - 0.35 * metal) + spec * tint * (1 + metal);\n out[i + c] = linearToSrgb8(v);\n }\n }\n }\n return out;\n}\n\n/** Cache of lit tiles for one layer, by the identity of their inputs. */\nexport class LitCache {\n private readonly tiles = new Map<TileKey, { inputs: unknown[]; tile: Tile }>();\n\n /** The layer's pixels lit (tiles without relief nearby are the colour tiles themselves). */\n lit(color: TiledSurface, ch: ThickChannels, light: PaintLighting): TiledSurface {\n if (!ch.height.tiles.size || light.relief <= 0) return color;\n const lk = lightingKey(light);\n const out = new Map(color.tiles);\n const keys = new Set<TileKey>();\n for (const key of color.tiles.keys()) keys.add(key);\n for (const [key, tile] of color.tiles) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const around = (dx: number, dy: number) => ch.height.tiles.get(tileKey(tx + dx, ty + dy))?.data ?? null;\n let any = false;\n for (let dy = -1; dy <= 1 && !any; dy++) for (let dx = -1; dx <= 1 && !any; dx++) if (around(dx, dy)) any = true;\n if (!any) continue;\n const h = ch.height.tiles.get(key) ?? null;\n const m = ch.material.tiles.get(key) ?? null;\n const inputs: unknown[] = [tile, lk, m];\n for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(ch.height.tiles.get(tileKey(tx + dx, ty + dy)) ?? null);\n const cached = this.tiles.get(key);\n if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {\n out.set(key, cached.tile);\n continue;\n }\n const hd = h ? expand(h, 1) : null;\n const md = m ? expand(m, 4) : null;\n const data = lightTile(expand(tile, 4), hd, (dx, dy) => {\n const t = ch.height.tiles.get(tileKey(tx + dx, ty + dy));\n return t ? expand(t, 1) : null;\n }, md, light);\n const lit: Tile = { data, uniform: null };\n this.tiles.set(key, { inputs, tile: lit });\n out.set(key, lit);\n }\n for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);\n return new TiledSurface(4, out, color.defaultValue);\n }\n}\n\n/** A tile's pixels as a full buffer (uniform tiles are stored compactly). */\nfunction expand(t: Tile, channels: 1 | 4): Uint8ClampedArray {\n if (t.data.length === TILE * TILE * channels) return t.data;\n const out = new Uint8ClampedArray(TILE * TILE * channels);\n const u = t.uniform ?? 0;\n if (channels === 1) out.fill(u & 255);\n else {\n const bytes = [(u >>> 24) & 255, (u >>> 16) & 255, (u >>> 8) & 255, u & 255];\n for (let i = 0; i < out.length; i += 4) out.set(bytes, i);\n }\n return out;\n}\n\nexport interface ThickStrokeInit {\n brush: PaintBrush;\n /** Where the colour goes (the session's live tiles). */\n target: PaintTileTarget;\n /** The layer's channels before the stroke. */\n channels: ThickChannels;\n /** Paint colour, sRGB 0..1. */\n color: RgbTriple;\n load: MixerLoad;\n seed: number;\n copies: readonly StrokeCopy[];\n /** Gradient paint (resolved): the colour varies along the impasto. */\n paint?: PaintGradientSource | null;\n}\n\ninterface CopyState {\n walker: PenDabWalker;\n exchange: PaintExchange;\n smudge: SmudgeTip | null;\n /** Height patch carried by the knife. */\n carried: Float32Array | null;\n amount: number;\n}\n\n/** One impasto stroke: colour through the exchange, height and material into live channel tiles. */\nexport class ThickStrokeDriver {\n dabCount = 0;\n private readonly smoother: PenSmoother;\n private readonly states: CopyState[];\n private readonly medium: TileMedium;\n private readonly height = new Map<TileKey, Uint8ClampedArray>();\n private readonly material = new Map<TileKey, Uint8ClampedArray>();\n private readonly paint: RgbTriple = [0, 0, 0];\n private tip = new Float32Array(0);\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n private version = 0;\n\n constructor(readonly init: ThickStrokeInit) {\n const b = init.brush;\n this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);\n const copies = init.copies.length ? init.copies : [IDENTITY_COPY];\n this.copies = copies;\n const rates = exchangeRates(b.behavior.pickup, b.behavior.mix, 1, b.behavior.mixing);\n this.states = copies.map((c, i) => ({\n walker: new PenDabWalker(b, (init.seed + i * 7919) | 0, c.linear),\n exchange: new PaintExchange(rates, init.load),\n smudge: b.impasto.tool === 'knife' ? new SmudgeTip({ strength: 0.85, length: 0.6 + 0.38 * b.impasto.drag, mixing: b.behavior.mixing }, b.tip.size * 1.2) : null,\n carried: null,\n amount: 1,\n }));\n this.medium = new TileMedium(init.target, {});\n // Across the tip makes no sense for a loaded impasto tip: the colour follows the dab instead.\n this.sampler = init.paint ? new PaintGradientSampler(init.paint.map.by === 'across-tip' ? { ...init.paint, map: { by: 'distance', length: 300, repeat: 'mirror' } } : init.paint, init.seed) : null;\n }\n\n private readonly copies: readonly StrokeCopy[];\n private readonly sampler: PaintGradientSampler | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n\n /** What copy 0 carries after the dabs so far. */\n get load(): MixerLoad {\n return this.states[0].exchange.load;\n }\n\n /** The channels with the stroke so far (new objects whenever they changed). */\n channels(): ThickChannels {\n const h = new Map(this.init.channels.height.tiles);\n const m = new Map(this.init.channels.material.tiles);\n for (const [k, d] of this.height) {\n const t = finishTile(d, 1, 0);\n if (t) h.set(k, t);\n else h.delete(k);\n }\n for (const [k, d] of this.material) {\n const t = finishTile(new Uint8ClampedArray(d), 4, 0);\n if (t) m.set(k, t);\n else m.delete(k);\n }\n void this.version;\n return { height: new TiledSurface(1, h, 0), material: new TiledSurface(4, m, 0) };\n }\n\n private heightTile(tx: number, ty: number, write: boolean): Uint8ClampedArray | null {\n const key = tileKey(tx, ty);\n let t = this.height.get(key);\n if (t) {\n // Live tiles are replaced on write so caches keyed by identity see the change.\n return t;\n }\n const base = this.init.channels.height.tiles.get(key);\n if (!write) return base ? expand(base, 1) : null;\n t = base ? new Uint8ClampedArray(expand(base, 1)) : new Uint8ClampedArray(TILE * TILE);\n this.height.set(key, t);\n return t;\n }\n\n private materialTile(tx: number, ty: number): Uint8ClampedArray {\n const key = tileKey(tx, ty);\n let t = this.material.get(key);\n if (!t) {\n const base = this.init.channels.material.tiles.get(key);\n t = base ? new Uint8ClampedArray(expand(base, 4)) : new Uint8ClampedArray(TILE * TILE * 4);\n this.material.set(key, t);\n }\n return t;\n }\n\n private getH(x: number, y: number): number {\n const t = this.heightTile(Math.floor(x / TILE), Math.floor(y / TILE), false);\n return t ? t[(y & 255) * TILE + (x & 255)] : 0;\n }\n\n private setH(x: number, y: number, v: number): void {\n const t = this.heightTile(Math.floor(x / TILE), Math.floor(y / TILE), true)!;\n t[(y & 255) * TILE + (x & 255)] = v;\n }\n\n push(points: readonly PenPoint[]): RasterRect[] {\n const rects: RasterRect[] = [];\n for (const raw of points) {\n this.lastRaw = raw;\n const p = this.smoother.push(raw);\n this.lastFed = p;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n this.states[i].walker.push(c.map(p), dabs);\n this.applyDabs(i, dabs, rects);\n });\n }\n return rects;\n }\n\n finish(): RasterRect[] {\n const rects: RasterRect[] = [];\n const raw = this.lastRaw;\n const fed = this.lastFed;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);\n this.states[i].walker.finish(dabs);\n this.applyDabs(i, dabs, rects);\n });\n return rects;\n }\n\n private applyDabs(copy: number, dabs: PenDab[], rects: RasterRect[]): void {\n for (const d of dabs) {\n const rect = dabRect(d, this.init.brush.tip.hardness);\n if (rect.width <= 0 || rect.height <= 0) continue;\n this.dab(copy, d, rect);\n rects.push(rect);\n this.dabCount++;\n }\n if (dabs.length) this.version++;\n }\n\n private dab(copy: number, d: PenDab, rect: RasterRect): void {\n const b = this.init.brush;\n const im = b.impasto;\n const st = this.states[copy];\n const n = rect.width * rect.height;\n if (this.tip.length < n) this.tip = new Float32Array(n);\n const tip = this.tip.subarray(0, n);\n rasterizeDab({ ...d, alpha: 1 }, b.tip.hardness, rect, tip);\n const sel = this.init.target.selection;\n if (sel) {\n for (let yy = 0, p = 0; yy < rect.height; yy++)\n for (let xx = 0; xx < rect.width; xx++, p++) {\n const mx = rect.x + xx;\n const my = rect.y + yy;\n if (tip[p] > 0) tip[p] *= mx >= 0 && my >= 0 && mx < sel.width && my < sel.height ? sel.data[my * sel.width + mx] / 255 : 0;\n }\n }\n const off = this.init.target.offset;\n const k = Math.min(1, d.alpha * d.opacity * b.tip.opacity) * st.amount;\n const thick = im.thickness * (1 - 0.8 * im.thinner);\n // The paint runs out along the stroke.\n st.amount *= 1 - 0.06 * (1 - im.load);\n // Across-the-stroke coordinate for bristle ridges and the roller's stipple.\n const nxs = Math.sin(d.direction);\n const nys = Math.cos(d.direction);\n const r = Math.max(0.5, d.radius);\n\n if (im.tool === 'knife') {\n // Scrape: the relief flattens towards its low points and is dragged along; the paint smears.\n st.smudge!.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: k }, rect, tip);\n this.knifeHeight(st, d, rect, tip, k, im.drag);\n this.paintMaterial(rect, tip, k * 0.6, im.gloss, im.metallic);\n return;\n }\n // Colour: the exchange mixes the brush paint with the wet paint it picks up (like oil).\n const ex = st.exchange;\n let flow = 1;\n if (this.sampler) {\n const g = this.sampler.dab(d, this.sample);\n ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);\n flow = g.alpha;\n }\n ex.pickUp(this.medium.sample(rect, tip));\n const total = ex.paint(this.paint);\n ex.drain();\n const dep = Math.min(1, total) * k * (1 - 0.5 * im.thinner) * flow;\n if (dep > 0) {\n const cov = new Float32Array(n);\n for (let p = 0; p < n; p++) cov[p] = tip[p] > 0 ? Math.min(1, tip[p] * (im.tool === 'roller' || im.tool === 'gloop' || im.tool === 'tube' ? 1.6 : 1.1)) : 0;\n this.medium.deposit(rect, cov, dep, this.paint, ex.paintPigment);\n }\n // Height.\n const seed = this.init.seed;\n for (let yy = 0, p = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, p++) {\n const c = tip[p];\n if (c <= 0) continue;\n const dx = rect.x + xx;\n const dy = rect.y + yy;\n const lx = dx - off.x;\n const ly = dy - off.y;\n const u = ((dx + 0.5 - d.x) * nxs + (dy + 0.5 - d.y) * nys) / r;\n const h0 = this.getH(lx, ly);\n let h = h0;\n const full = 255 * thick * k;\n switch (im.tool) {\n case 'brush': {\n // Bristle ridges along the stroke: stripes across the tip, with a little noise.\n const ridge = 0.6 + 0.4 * Math.sin(u * 9 + hash2(Math.round(u * 6), 3, seed) * 6.28) + 0.15 * (hash2(lx, ly, seed) - 0.5);\n const target = full * c * ridge;\n // Wet paint pushed by the bristles: towards the ridge profile, building where it overlaps.\n h = Math.max(h0 * (1 - 0.25 * c * im.drag), Math.min(255, h0 * 0.35 + target));\n break;\n }\n case 'tube': {\n const dome = Math.sqrt(Math.max(0, c));\n h = Math.max(h0, Math.min(255, full * dome * 1.1));\n break;\n }\n case 'gloop': {\n const dome = Math.sqrt(Math.max(0, 1 - Math.min(1, Math.abs(u)) ** 2));\n h = Math.max(h0, Math.min(255, full * dome * (c > 0.2 ? 1 : c * 5)));\n break;\n }\n case 'roller': {\n const stipple = 0.75 + 0.5 * hash2(lx >> 1, ly >> 1, seed);\n h = h0 + (Math.min(255, full * 0.6 * stipple) - h0) * Math.min(1, c * 1.5);\n break;\n }\n }\n this.setH(lx, ly, Math.round(Math.max(0, Math.min(255, h))));\n }\n }\n this.paintMaterial(rect, tip, k, im.gloss * (1 - 0.3 * im.thinner), im.metallic);\n }\n\n /** The knife: flattens the relief under it and carries some of it along (a patch, like a smudge). */\n private knifeHeight(st: CopyState, d: PenDab, rect: RasterRect, tip: Float32Array, k: number, drag: number): void {\n const off = this.init.target.offset;\n const w = rect.width;\n const n = w * rect.height;\n const cur = new Float32Array(n);\n for (let yy = 0, p = 0; yy < rect.height; yy++) for (let xx = 0; xx < w; xx++, p++) cur[p] = this.getH(rect.x + xx - off.x, rect.y + yy - off.y);\n const carried = st.carried && st.carried.length === n ? st.carried : null;\n const next = new Float32Array(n);\n for (let yy = 0, p = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++, p++) {\n const c = tip[p] * k;\n if (c <= 0) {\n next[p] = cur[p];\n continue;\n }\n // Scrape flat: the blade levels to a thin film; what it carries is laid down at the trailing edge.\n const flat = cur[p] * (1 - 0.85 * c);\n const laid = carried ? carried[p] * drag * c : 0;\n const v = Math.max(flat, Math.min(255, flat * 0.5 + laid));\n next[p] = v;\n this.setH(rect.x + xx - off.x, rect.y + yy - off.y, Math.round(v));\n }\n }\n // What the blade picked up: the paint it scraped off.\n const pick = new Float32Array(n);\n for (let p = 0; p < n; p++) pick[p] = Math.max(0, cur[p] - next[p]) + (carried ? carried[p] * (1 - drag) * 0.5 : 0);\n st.carried = pick;\n }\n\n private paintMaterial(rect: RasterRect, tip: Float32Array, k: number, gloss: number, metal: number): void {\n const off = this.init.target.offset;\n for (let yy = 0, p = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, p++) {\n const c = Math.min(1, tip[p] * k);\n if (c <= 0) continue;\n const lx = rect.x + xx - off.x;\n const ly = rect.y + yy - off.y;\n const t = this.materialTile(Math.floor(lx / TILE), Math.floor(ly / TILE));\n const i = ((ly & 255) * TILE + (lx & 255)) * 4;\n t[i] = Math.round(t[i] + (gloss * 255 - t[i]) * c);\n t[i + 1] = Math.round(t[i + 1] + (metal * 255 - t[i + 1]) * c);\n t[i + 3] = 255;\n }\n }\n }\n}\n\n/** Height statistics of a layer's relief (for tools). */\nexport function reliefInfo(ch: ThickChannels): { bounds: RasterRect | null; maxHeight: number; tiles: number } {\n let max = 0;\n for (const t of ch.height.tiles.values()) {\n if (t.uniform !== null) max = Math.max(max, t.uniform & 255);\n else for (let i = 0; i < t.data.length; i++) if (t.data[i] > max) max = t.data[i];\n }\n return { bounds: ch.height.bounds, maxHeight: max, tiles: ch.height.tiles.size };\n}\n\n/** A layer's thick-paint channels as saved: PNG data URLs over a rectangle (layer space). */\nexport interface SavedThickChannels {\n rect: RasterRect;\n /** Greyscale PNG of the height. */\n height: string;\n /** RGBA PNG of the material (R gloss, G metallic). */\n material: string;\n}\n\n/** Channels → saved form (`null` when there is no relief). */\nexport function thickToJSON(ch: ThickChannels): SavedThickChannels | null {\n const b = ch.height.bounds;\n if (!b) return null;\n return { rect: { ...b }, height: pngDataUrl(b.width, b.height, readRegion(ch.height, b), 1), material: pngDataUrl(b.width, b.height, readRegion(ch.material, b), 4) };\n}\n\n/** Saved form → channels; throws a sentence when it cannot be read. */\nexport function thickFromJSON(raw: unknown): ThickChannels {\n const r = raw as Partial<SavedThickChannels>;\n if (!r || typeof r !== 'object' || !r.rect || typeof r.height !== 'string' || typeof r.material !== 'string') throw new Error('not saved thick-paint channels');\n const decode = (url: string, gray: boolean) => {\n const parsed = parseDataUrl(url);\n if (!parsed) throw new Error('a channel is not a PNG data URL');\n return decodePng(parsed.bytes, { keepGray: gray });\n };\n const h = decode(r.height, true);\n const m = decode(r.material, false);\n const hd = h.channels === 1 ? h.data : Uint8ClampedArray.from({ length: h.width * h.height }, (_, i) => h.data[i * 4]);\n return {\n height: surfaceFromPixels({ width: h.width, height: h.height, data: hd }, 1, r.rect.x, r.rect.y),\n material: surfaceFromPixels({ width: m.width, height: m.height, data: m.data }, 4, r.rect.x, r.rect.y),\n };\n}\n","/**\n * A live stroke on one pixel layer. Points arrive while the pen moves; the layer shows the stroke\n * so far through a live surface (no history); `end()` commits it as one undo step.\n *\n * - Dab and eraser brushes: the coverage is cumulative, so each update recomposites the touched\n * region from the pre-stroke tile (`applyStrokeToTile` with `into` and `region`).\n * - Dab-by-dab brushes (`PaintDabStroke`: the Mixer Brush, blender, smudge, and dab brushes that\n * pick up paint or are multi-coloured): dabs read what earlier dabs laid down, so they paint\n * into live tile copies; every tile written since the last update gets a new `Tile` object,\n * because the compositor caches by tile identity.\n */\n\nimport { applyStrokeToTile, type StrokeApply } from '../paint/brush';\nimport { EMPTY_MIXER_LOAD, type PaintTileTarget } from '../paint/exchange';\nimport { cleanMixer, loadMixer, type MixerLoad } from '../paint/mixer';\nimport { TILE, TiledSurface, finishTile, tileKey, tilesInRect, type RasterRect, type Tile, type TileKey } from '../raster/surface';\nimport { usesPaintExchange, type PaintBrush } from './brushes';\nimport type { PaintGradientSource } from '../paint/gradient-paint';\nimport { PaintDabStroke } from './dab-stroke';\nimport { DryStroke, IDENTITY_COPY, type PenPoint, type StrokeCopy } from './engine';\nimport type { PaperGrain } from './paper';\nimport type { Affine } from './strokes';\nimport type { WetStrokeDriver } from './wet';\nimport { ThickStrokeDriver, type ThickChannels } from './thick';\nimport { BristleStroke, ParticleStroke, type CoverageStroke } from './procedural';\nimport { StickerSpray, type Sticker } from './stickers';\n\nexport interface StrokeSessionHost {\n /** Shows the layer's live surface (no history); `rects` are where it changed. */\n update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;\n /** Ends the stroke: `surface` is the new pixels, or `null` when nothing changed / it was cancelled. */\n commit(session: PaintStrokeSession, surface: TiledSurface | null, cancelled: boolean): void;\n}\n\nexport interface StrokeSessionInit {\n layerId: string;\n surface: TiledSurface;\n offset: { x: number; y: number };\n lockTransparency: boolean;\n brush: PaintBrush;\n /** Paint colour 0..255 RGBA. */\n color: readonly [number, number, number, number];\n /** What the eraser paints on a transparency-locked layer (the paper colour). */\n background: readonly [number, number, number, number];\n grain: PaperGrain | null;\n paperDepth: number;\n seed: number;\n /** The paint colour as #rrggbb (recorded with the stroke). */\n colorHex: string;\n /** The stroke's extra copies as affine maps (recorded with the stroke); `copies` holds all of them as engine copies. */\n affineCopies: readonly Affine[];\n copies: readonly StrokeCopy[];\n /** Applied to each raw sample before it is painted and recorded (perspective snapping). */\n inputFilter?: (p: PenPoint) => PenPoint;\n /** Mixer and wet-capable dab brushes: what the brush carries at the start of the stroke. */\n mixerLoad: MixerLoad;\n /** The secondary colour 0..255 RGBA (a multi-colour brush without its own colours uses it). */\n secondary?: readonly [number, number, number, number];\n /**\n * Wet brushes: the driver that applies the stroke to the wet layer, and the layer's current\n * pixels (the wet layer's display). The session's own surface is not used.\n */\n wet?: { driver: WetStrokeDriver; surface: () => TiledSurface };\n /** Impasto brushes: the layer's thick-paint channels before the stroke. */\n thick?: { channels: ThickChannels };\n /** Where paint may land (canvas-sized coverage 0..255: the stencils' mask), or none. */\n selection?: { width: number; height: number; data: ArrayLike<number> } | null;\n /** Sticker brushes: the next sticker number on the layer. */\n sticker?: { nextId: number };\n /** Gradient paint (resolved: concrete colours), or `null` for the one colour. Recorded with the stroke. */\n paint?: PaintGradientSource | null;\n}\n\nexport interface PaintStrokeResult {\n layerId: string;\n /** Document rectangle the stroke touched, or `null` when no pixel changed. */\n bounds: RasterRect | null;\n dabs: number;\n /** Id of the recorded stroke (see `list_strokes` / `replay_strokes`), or `null` when nothing changed. */\n strokeId: string | null;\n /** Mixer and wet-capable dab brushes: what the brush carries after the stroke (cleaned unless it is dirty). */\n mixerLoad?: MixerLoad;\n /** Wet brushes: the paint is wet (it keeps moving until it dries; see dry_paint / advance_wet_paint). */\n wet?: boolean;\n}\n\nfunction defaultTile(surface: TiledSurface): Uint8ClampedArray {\n const data = new Uint8ClampedArray(TILE * TILE * surface.channels);\n if (surface.defaultValue) data.fill(surface.defaultValue);\n return data;\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\nfunction union(a: RasterRect | null, b: RasterRect): RasterRect {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\nexport class PaintStrokeSession {\n readonly layerId: string;\n /** The raw input so far (what a replay feeds back in). */\n readonly points: PenPoint[] = [];\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private readonly fresh = new Set<TileKey>();\n private readonly dry: CoverageStroke | null = null;\n private readonly wet: PaintDabStroke | null = null;\n private readonly wetDriver: WetStrokeDriver | null = null;\n private readonly thickDriver: ThickStrokeDriver | null = null;\n private readonly stickerSpray: StickerSpray | null = null;\n /** A wet stroke that ended while the simulation was mid-batch: it commits when the driver catches up. */\n private wetEndPending = false;\n private readonly apply: StrokeApply;\n private readonly copies: readonly StrokeCopy[];\n private bounds: RasterRect | null = null;\n private lastRaw: PenPoint | null = null;\n private closed = false;\n result: PaintStrokeResult | null = null;\n /** The mask stencils this stroke was painted with when they are not the editor's (replays). */\n maskStencils: readonly import('./stencils').Stencil[] | null = null;\n\n constructor(private readonly host: StrokeSessionHost, readonly init: StrokeSessionInit) {\n this.layerId = init.layerId;\n this.base = init.surface;\n this.live = new Map(this.base.tiles);\n this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];\n const brush = init.brush;\n if (init.wet) this.wetDriver = init.wet.driver;\n this.apply = {\n mode: brush.engine === 'eraser' ? 'erase' : 'paint',\n color: init.color,\n background: init.background,\n opacity: brush.tip.opacity,\n blend: brush.tip.blend,\n lockTransparency: init.lockTransparency,\n selection: init.selection ?? null,\n offset: init.offset,\n };\n if (this.wetDriver) {\n // the wet layer paints; nothing else to set up\n } else if (init.sticker) {\n this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null });\n } else if (init.thick) {\n const rgb = (c: readonly number[]): [number, number, number] => [c[0] / 255, c[1] / 255, c[2] / 255];\n this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null });\n } else if (brush.engine === 'mixer' || brush.engine === 'blender' || brush.engine === 'smudge' || usesPaintExchange(brush, Boolean(init.paint))) {\n const rgb = (c: readonly number[]): [number, number, number] => [c[0] / 255, c[1] / 255, c[2] / 255];\n this.wet = new PaintDabStroke(brush, {\n target: this.tileTarget(),\n color: rgb(init.color),\n secondary: rgb(init.secondary ?? init.background),\n load: init.mixerLoad,\n grain: init.grain,\n paperDepth: init.paperDepth,\n seed: init.seed,\n copies: this.copies,\n paint: init.paint ?? null,\n });\n } else {\n const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null };\n this.dry = brush.engine === 'particle' ? new ParticleStroke(brush, opts) : brush.engine === 'bristle' ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);\n }\n }\n\n private tileTarget(): PaintTileTarget {\n return {\n offset: this.init.offset,\n lockTransparency: this.init.lockTransparency,\n selection: this.init.selection ?? null,\n readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? 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 open(): boolean {\n return !this.closed;\n }\n\n /** Adds raw pen samples (document pixels). */\n push(points: readonly PenPoint[]): void {\n if (this.closed || !points.length) return;\n const filter = this.init.inputFilter;\n if (filter) points = points.map(filter);\n for (const p of points) this.points.push(p);\n this.lastRaw = points[points.length - 1];\n if (this.wetDriver) {\n this.wetUpdate(this.wetDriver.push(points));\n return;\n }\n this.update(this.dry ? this.dry.push(points) : this.thickDriver ? this.thickDriver.push(points) : this.stickerSpray ? this.stickerSpray.push(points) : this.wet!.push(points));\n }\n\n /** Sticker strokes: the stickers sprayed so far. */\n stickers(): Sticker[] | null {\n return this.stickerSpray ? this.stickerSpray.stickers.map((s) => ({ ...s })) : null;\n }\n\n /** Impasto strokes: the layer's thick-paint channels with the stroke so far. */\n thickChannels(): ThickChannels | null {\n return this.thickDriver?.channels() ?? null;\n }\n\n /** Shows a wet stroke's progress (the wet layer's display). */\n wetUpdate(rects: RasterRect[]): void {\n if (!rects.length || !this.init.wet) return;\n for (const r of rects) this.bounds = union(this.bounds, r);\n this.host.update(this.layerId, this.init.wet.surface(), rects);\n }\n\n /** Whether a wet stroke has ended but waits for the simulation to finish a batch. */\n get pendingEnd(): boolean {\n return this.wetEndPending;\n }\n\n /** Completes a wet stroke whose end waited (the controller calls it when the driver is done). */\n completeWetEnd(): void {\n if (!this.wetEndPending || !this.wetDriver || !this.wetDriver.done) return;\n this.wetEndPending = false;\n this.finishWet();\n }\n\n private finishWet(): void {\n const d = this.wetDriver!;\n const changed = d.dabCount > 0;\n this.result = { layerId: this.layerId, bounds: changed ? d.bounds : null, dabs: d.dabCount, strokeId: null, wet: true };\n const carried = d.load;\n if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);\n this.host.commit(this, changed ? this.init.wet!.surface() : null, false);\n }\n\n private update(rects: RasterRect[]): void {\n if (!rects.length) return;\n for (const r of rects) this.bounds = union(this.bounds, r);\n const ch = this.base.channels;\n if (!this.dry) {\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.host.update(this.layerId, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);\n return;\n }\n const off = this.init.offset;\n const regions = new Map<TileKey, { tx: number; ty: number; r: RasterRect }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - off.x, y: r.y - off.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 ? union(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(this.dry.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.host.update(this.layerId, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Ends the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new Error('This stroke has already ended.');\n if (this.wetDriver) {\n this.wetUpdate(this.wetDriver.finish());\n this.closed = true;\n if (this.wetDriver.done) this.finishWet();\n else {\n this.wetEndPending = true;\n this.result = { layerId: this.layerId, bounds: this.bounds, dabs: this.wetDriver.dabCount, strokeId: null, wet: true };\n }\n return this.result!;\n }\n this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet!.finish());\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 tile = finishTile(this.live.get(key)!.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 // An impasto stroke can change only the relief (a knife on dry colour).\n if (this.thickDriver && this.thickDriver.dabCount > 0) changed = true;\n const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;\n const dabs = this.dry ? this.dry.dabCount : this.thickDriver ? this.thickDriver.dabCount : this.stickerSpray ? this.stickerSpray.dabCount : this.wet!.dabCount;\n this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };\n const carried = this.wet?.load ?? this.thickDriver?.load;\n if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);\n this.host.commit(this, surface, false);\n return this.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.host.commit(this, null, true);\n }\n}\n\n/** The Mixer Brush load a stroke starts with. */\nexport function mixerLoadForStroke(brush: PaintBrush, current: MixerLoad, color: readonly [number, number, number]): MixerLoad {\n if (!brush.loadsColor) return { reservoir: null, amount: 0, pickup: brush.mixer.cleanEachStroke ? null : current.pickup };\n if (brush.mixer.loadEachStroke || !current.reservoir) return loadMixer(current, color);\n return current;\n}\n\n/**\n * What any brush carries at the start of a stroke: the Mixer Brush as `mixerLoadForStroke`; a\n * wet-capable dab brush the paint colour, full, plus the carried paint when it is dirty; other\n * brushes nothing.\n */\nexport function brushLoadForStroke(brush: PaintBrush, current: MixerLoad, color: readonly [number, number, number]): MixerLoad {\n if (brush.engine === 'mixer') return mixerLoadForStroke(brush, current, color);\n if (brush.engine === 'impasto') return { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null };\n if (brush.engine === 'wet') return brush.wet.tool === 'paint' ? { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null } : EMPTY_MIXER_LOAD;\n if (usesPaintExchange(brush)) return { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null };\n return EMPTY_MIXER_LOAD;\n}\n","/**\n * Stencils: shapes laid on the canvas that guide or mask strokes, like a ruler, a French curve or a\n * masking template held over the paper.\n *\n * - `ruler` (a straight edge) and `arc` (a circle or part of one) **guide**: a stroke that starts\n * within `snap` pixels of the edge follows it exactly; one that starts further away is free.\n * - `line` (a straight edge across the canvas) and `shape` (rectangle, ellipse or polygon)\n * **mask**: paint lands only on one side of the line, or only inside the shape (outside with\n * `invert`), with an anti-aliased edge. Several masks combine (paint lands where all allow it).\n * - Any stencil can be switched to the other `mode`: a shape can guide (strokes follow its\n * outline) and a ruler can mask (paint on one side of it).\n *\n * Geometry only; `stencilMask` makes the canvas coverage the stroke engines read as a selection.\n */\n\nimport type { Point2 } from './guides';\n\nexport type StencilKind = 'ruler' | 'arc' | 'line' | 'shape';\nexport const STENCIL_KINDS: readonly StencilKind[] = ['ruler', 'arc', 'line', 'shape'];\n\nexport type StencilShape = 'rect' | 'ellipse' | 'polygon';\nexport const STENCIL_SHAPES: readonly StencilShape[] = ['rect', 'ellipse', 'polygon'];\n\nexport interface Stencil {\n id: string;\n kind: StencilKind;\n /** `guide`: strokes that start near the edge follow it; `mask`: paint lands only inside (or on one side). */\n mode: 'guide' | 'mask';\n /** Shown and in effect (default true). */\n enabled: boolean;\n /** ruler / line: two points on the edge (document pixels). */\n from?: Point2;\n to?: Point2;\n /** arc: centre and radius, and the part of the circle in degrees clockwise from +x (default the whole circle). */\n center?: Point2;\n radius?: number;\n startAngle?: number;\n endAngle?: number;\n /** shape: its kind and geometry (`rect` for rect and ellipse, `points` for a polygon). */\n shape?: StencilShape;\n rect?: { x: number; y: number; width: number; height: number };\n points?: Point2[];\n /** mask: paint outside the shape (or on the other side of the line) instead. */\n invert: boolean;\n /** guide: how near the edge (px) a stroke must start to follow it (default 40). */\n snap: number;\n /** mask: soft edge width in pixels (default 1, anti-aliased). */\n feather: number;\n}\n\nexport type StencilInput = Partial<Omit<Stencil, 'id'>> & { kind: StencilKind };\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst pt = (p: unknown): Point2 | undefined => (p && finite((p as Point2).x) && finite((p as Point2).y) ? { x: (p as Point2).x, y: (p as Point2).y } : undefined);\n\n/** A stencil with every field in range; throws a sentence naming what is missing. */\nexport function normalizeStencil(raw: StencilInput, id: string, base?: Stencil): Stencil {\n const kind = STENCIL_KINDS.includes(raw.kind) ? raw.kind : base?.kind;\n if (!kind) throw new Error(`kind ${JSON.stringify(raw.kind)} is not a stencil; use ${STENCIL_KINDS.join(', ')}.`);\n const s: Stencil = {\n id,\n kind,\n mode: raw.mode === 'guide' || raw.mode === 'mask' ? raw.mode : (base?.mode ?? (kind === 'ruler' || kind === 'arc' ? 'guide' : 'mask')),\n enabled: typeof raw.enabled === 'boolean' ? raw.enabled : (base?.enabled ?? true),\n invert: typeof raw.invert === 'boolean' ? raw.invert : (base?.invert ?? false),\n snap: finite(raw.snap) ? Math.min(400, Math.max(1, raw.snap)) : (base?.snap ?? 40),\n feather: finite(raw.feather) ? Math.min(100, Math.max(0, raw.feather)) : (base?.feather ?? 1),\n };\n if (kind === 'ruler' || kind === 'line') {\n s.from = pt(raw.from) ?? base?.from;\n s.to = pt(raw.to) ?? base?.to;\n if (!s.from || !s.to) throw new Error(`A ${kind} needs from and to points ({x, y} in document pixels).`);\n if (Math.hypot(s.to.x - s.from.x, s.to.y - s.from.y) < 1) throw new Error(`A ${kind}'s from and to must be at least 1 px apart.`);\n } else if (kind === 'arc') {\n s.center = pt(raw.center) ?? base?.center;\n s.radius = finite(raw.radius) ? raw.radius : base?.radius;\n if (!s.center || !(s.radius! > 0)) throw new Error('An arc needs a center ({x, y}) and a radius above 0.');\n s.startAngle = finite(raw.startAngle) ? raw.startAngle : (base?.startAngle ?? 0);\n s.endAngle = finite(raw.endAngle) ? raw.endAngle : (base?.endAngle ?? 360);\n } else {\n s.shape = STENCIL_SHAPES.includes(raw.shape as StencilShape) ? raw.shape : (base?.shape ?? 'rect');\n if (s.shape === 'polygon') {\n const pts = Array.isArray(raw.points) ? raw.points.map(pt).filter((p): p is Point2 => Boolean(p)) : base?.points;\n if (!pts || pts.length < 3) throw new Error('A polygon stencil needs at least 3 points.');\n s.points = pts;\n } else {\n const r = raw.rect ?? base?.rect;\n if (!r || !finite(r.x) || !finite(r.y) || !(r.width > 0) || !(r.height > 0)) throw new Error(`A ${s.shape} stencil needs rect {x, y, width, height} with a width and height above 0.`);\n s.rect = { x: r.x, y: r.y, width: r.width, height: r.height };\n }\n }\n return s;\n}\n\n/** The outline of a stencil as a polyline (document pixels), for drawing and for shape guides. */\nexport function stencilOutline(s: Stencil): Point2[] {\n if (s.kind === 'ruler' || s.kind === 'line') return [s.from!, s.to!];\n if (s.kind === 'arc') {\n const a0 = (s.startAngle! * Math.PI) / 180;\n const a1 = (s.endAngle! * Math.PI) / 180;\n const n = Math.max(8, Math.ceil(Math.abs(a1 - a0) * Math.max(8, s.radius! / 4)));\n return Array.from({ length: n + 1 }, (_, i) => {\n const a = a0 + ((a1 - a0) * i) / n;\n return { x: s.center!.x + Math.cos(a) * s.radius!, y: s.center!.y + Math.sin(a) * s.radius! };\n });\n }\n if (s.shape === 'polygon') return [...s.points!, s.points![0]];\n const r = s.rect!;\n if (s.shape === 'rect') return [{ x: r.x, y: r.y }, { x: r.x + r.width, y: r.y }, { x: r.x + r.width, y: r.y + r.height }, { x: r.x, y: r.y + r.height }, { x: r.x, y: r.y }];\n const n = 96;\n return Array.from({ length: n + 1 }, (_, i) => {\n const a = (i / n) * Math.PI * 2;\n return { x: r.x + r.width / 2 + (Math.cos(a) * r.width) / 2, y: r.y + r.height / 2 + (Math.sin(a) * r.height) / 2 };\n });\n}\n\n/** The nearest point of a polyline to `p`, and its distance. */\nfunction nearestOnPolyline(line: readonly Point2[], p: Point2): { point: Point2; distance: number } {\n let best = { point: line[0], distance: Infinity };\n for (let i = 0; i + 1 < line.length; i++) {\n const a = line[i];\n const b = line[i + 1];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len2 = dx * dx + dy * dy || 1;\n const t = Math.min(1, Math.max(0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));\n const q = { x: a.x + dx * t, y: a.y + dy * t };\n const d = Math.hypot(p.x - q.x, p.y - q.y);\n if (d < best.distance) best = { point: q, distance: d };\n }\n return best;\n}\n\n/** The nearest point on a stencil's edge (a ruler extends without end; an arc is exact). */\nfunction nearestOnStencil(s: Stencil, p: Point2): { point: Point2; distance: number } {\n if (s.kind === 'ruler' || s.kind === 'line') {\n const a = s.from!;\n const dx = s.to!.x - a.x;\n const dy = s.to!.y - a.y;\n const len2 = dx * dx + dy * dy;\n const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;\n const q = { x: a.x + dx * t, y: a.y + dy * t };\n return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };\n }\n if (s.kind === 'arc') {\n const c = s.center!;\n const ang = Math.atan2(p.y - c.y, p.x - c.x);\n const full = Math.abs(s.endAngle! - s.startAngle!) >= 360;\n if (full) {\n const q = { x: c.x + Math.cos(ang) * s.radius!, y: c.y + Math.sin(ang) * s.radius! };\n return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };\n }\n return nearestOnPolyline(stencilOutline(s), p);\n }\n return nearestOnPolyline(stencilOutline(s), p);\n}\n\n/**\n * Snaps a stroke to the guide stencils: the first point picks the nearest guide within its `snap`\n * distance (or none), and every point after it is moved onto that guide's edge.\n */\nexport class StencilSnapper {\n private guide: Stencil | null | undefined;\n\n constructor(private readonly stencils: readonly Stencil[]) {}\n\n /** The stencil the stroke follows, once chosen (`null`: none). */\n get following(): Stencil | null {\n return this.guide ?? null;\n }\n\n snap<P extends Point2>(p: P): P {\n if (this.guide === undefined) {\n let best: { s: Stencil; d: number } | null = null;\n for (const s of this.stencils) {\n if (!s.enabled || s.mode !== 'guide') continue;\n const { distance } = nearestOnStencil(s, p);\n if (distance <= s.snap && (!best || distance < best.d)) best = { s, d: distance };\n }\n this.guide = best?.s ?? null;\n }\n if (!this.guide) return p;\n const q = nearestOnStencil(this.guide, p).point;\n return { ...p, x: q.x, y: q.y };\n }\n}\n\n/** Signed distance to a mask stencil's edge: positive where paint may land. */\nfunction maskDistance(s: Stencil, x: number, y: number): number {\n let d: number;\n if (s.kind === 'ruler' || s.kind === 'line') {\n // The left side of from → to (screen coordinates, y down) is \"inside\".\n const dx = s.to!.x - s.from!.x;\n const dy = s.to!.y - s.from!.y;\n const len = Math.hypot(dx, dy);\n d = ((x - s.from!.x) * dy - (y - s.from!.y) * dx) / len;\n } else if (s.kind === 'arc') {\n d = s.radius! - Math.hypot(x - s.center!.x, y - s.center!.y);\n } else if (s.shape === 'rect') {\n const r = s.rect!;\n d = Math.min(x - r.x, r.x + r.width - x, y - r.y, r.y + r.height - y);\n } else if (s.shape === 'ellipse') {\n const r = s.rect!;\n const rx = r.width / 2;\n const ry = r.height / 2;\n const nx = (x - r.x - rx) / rx;\n const ny = (y - r.y - ry) / ry;\n // Approximate distance: the radial distance scaled by the local radius.\n const k = Math.hypot(nx, ny);\n d = (1 - k) * Math.min(rx, ry);\n } else {\n const pts = s.points!;\n let inside = false;\n for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {\n const a = pts[i];\n const b = pts[j];\n if (a.y > y !== b.y > y && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x) inside = !inside;\n }\n const e = nearestOnPolyline([...pts, pts[0]], { x, y }).distance;\n d = inside ? e : -e;\n }\n return s.invert ? -d : d;\n}\n\n/**\n * The combined coverage of the enabled mask stencils over a `width × height` canvas (0..255 per\n * pixel), or `null` when no stencil masks.\n */\nexport function stencilMask(stencils: readonly Stencil[], width: number, height: number): { width: number; height: number; data: Uint8Array } | null {\n const masks = stencils.filter((s) => s.enabled && s.mode === 'mask');\n if (!masks.length) return null;\n const data = new Uint8Array(width * height);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n let cov = 1;\n for (const s of masks) {\n const f = Math.max(0.5, s.feather);\n const d = maskDistance(s, x + 0.5, y + 0.5);\n cov = Math.min(cov, Math.min(1, Math.max(0, d / (2 * f) + 0.5)));\n if (cov <= 0) break;\n }\n data[y * width + x] = Math.round(cov * 255);\n }\n }\n return { width, height, data };\n}\n\n/** A stencil scaled about the origin by `k` (for repainting at a larger size). */\nexport function scaleStencil(s: Stencil, k: number): Stencil {\n const p = (q?: Point2) => (q ? { x: q.x * k, y: q.y * k } : undefined);\n return {\n ...s,\n ...(s.from ? { from: p(s.from) } : {}),\n ...(s.to ? { to: p(s.to) } : {}),\n ...(s.center ? { center: p(s.center), radius: s.radius! * k } : {}),\n ...(s.rect ? { rect: { x: s.rect.x * k, y: s.rect.y * k, width: s.rect.width * k, height: s.rect.height * k } } : {}),\n ...(s.points ? { points: s.points.map((q) => p(q)!) } : {}),\n snap: s.snap * k,\n feather: s.feather * k,\n };\n}\n","/**\n * Strokes as data. Every committed stroke is recorded with the brush, colour, raw pen samples,\n * jitter seed and symmetry copies it was painted with, so it replays to the same pixels, and can\n * be replayed moved, scaled, rotated, recoloured or with another brush (`replay_strokes`).\n */\n\nimport { normalizePaintBrush, type PaintBrushInput, type PaintBrush } from './brushes';\nimport type { Linear2, PenPoint, StrokeCopy } from './engine';\nimport type { MixerLoad } from '../paint/exchange';\nimport { normalizeStencil, type Stencil } from './stencils';\nimport { readPaintSource, scalePaintGradient, type PaintGradientSource } from '../paint/gradient-paint';\n\n/** An affine map [a, b, c, d, e, f]: (x, y) → (a·x + b·y + e, c·x + d·y + f). */\nexport type Affine = readonly [number, number, number, number, number, number];\n\nexport const IDENTITY_AFFINE: Affine = [1, 0, 0, 1, 0, 0];\n\nexport interface RecordedStroke {\n id: string;\n layerId: string;\n brush: PaintBrush;\n /** #rrggbb */\n color: string;\n /** Raw pen samples in document pixels, before smoothing. */\n points: PenPoint[];\n seed: number;\n /** Extra copies painted with the stroke (mirror / kaleidoscope), as affine maps of the points. */\n copies?: Affine[];\n /** What the brush carried at the start (Mixer Brush and wet-capable brushes), so a replay mixes the same. */\n load?: MixerLoad;\n /** Wet brushes: the wet simulation's clock (steps) at which each point was applied, so a replay lets the paint flow the same. */\n steps?: number[];\n /** The mask stencils it was painted through (so a replay keeps to them). */\n stencils?: Stencil[];\n /** Gradient paint, with concrete colours (`color` is then its first stop's colour, for older readers). */\n paint?: PaintGradientSource;\n}\n\nexport function applyAffine(m: Affine, x: number, y: number): { x: number; y: number } {\n return { x: m[0] * x + m[1] * y + m[4], y: m[2] * x + m[3] * y + m[5] };\n}\n\n/** `a ∘ b` (b first). */\nexport function composeAffine(a: Affine, b: Affine): Affine {\n return [\n a[0] * b[0] + a[1] * b[2],\n a[0] * b[1] + a[1] * b[3],\n a[2] * b[0] + a[3] * b[2],\n a[2] * b[1] + a[3] * b[3],\n a[0] * b[4] + a[1] * b[5] + a[4],\n a[2] * b[4] + a[3] * b[5] + a[5],\n ];\n}\n\nexport function invertAffine(m: Affine): Affine {\n const det = m[0] * m[3] - m[1] * m[2];\n if (Math.abs(det) < 1e-12) throw new Error('The transform is not invertible (scale 0?).');\n const a = m[3] / det;\n const b = -m[1] / det;\n const c = -m[2] / det;\n const d = m[0] / det;\n return [a, b, c, d, -(a * m[4] + b * m[5]), -(c * m[4] + d * m[5])];\n}\n\n/** The pen sample mapped by `m` (tilt and twist are kept: they describe the pen, not the path). */\nexport function mapPenPoint(m: Affine, p: PenPoint): PenPoint {\n const q = applyAffine(m, p.x, p.y);\n return { ...p, x: q.x, y: q.y };\n}\n\n/** An affine copy as the engine's `StrokeCopy`. */\nexport function affineCopy(m: Affine): StrokeCopy {\n const linear: Linear2 = [m[0], m[1], m[2], m[3]];\n return { map: (p) => mapPenPoint(m, p), linear };\n}\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\n\nfunction readPoint(raw: unknown): PenPoint | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n if (!finite(r.x) || !finite(r.y)) return null;\n const p: PenPoint = { x: r.x, y: r.y };\n for (const k of ['pressure', 't', 'tiltX', 'tiltY', 'altitude', 'azimuth', 'twist'] as const) if (finite(r[k])) p[k] = r[k] as number;\n return p;\n}\n\nfunction readAffine(raw: unknown): Affine | null {\n return Array.isArray(raw) && raw.length === 6 && raw.every(finite) ? (raw as unknown as Affine) : null;\n}\n\nfunction readRgb(raw: unknown): [number, number, number] | null {\n return Array.isArray(raw) && raw.length === 3 && raw.every(finite) ? (raw.map((v) => Math.min(1, Math.max(0, v))) as [number, number, number]) : null;\n}\n\nfunction readLoad(raw: unknown): MixerLoad | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const reservoir = readRgb(r.reservoir);\n const pickup = readRgb(r.pickup);\n if (!reservoir && !pickup) return null;\n return { reservoir, amount: reservoir && finite(r.amount) ? Math.min(1, Math.max(0, r.amount)) : 0, pickup };\n}\n\n/** A recorded stroke read from JSON, or `null` when it is not one. */\nexport function normalizeBrushStroke(raw: unknown): RecordedStroke | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n if (typeof r.id !== 'string' || typeof r.layerId !== 'string' || !Array.isArray(r.points)) return null;\n const points = r.points.map(readPoint).filter((p): p is PenPoint => p !== null);\n if (!points.length) return null;\n const out: RecordedStroke = {\n id: r.id,\n layerId: r.layerId,\n brush: normalizePaintBrush((r.brush as PaintBrushInput) ?? {}),\n color: typeof r.color === 'string' && /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : '#000000',\n points,\n seed: finite(r.seed) ? r.seed | 0 : 0,\n };\n const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m): m is Affine => m !== null) : [];\n if (copies.length) out.copies = copies;\n const load = readLoad(r.load);\n if (load) out.load = load;\n if (Array.isArray(r.stencils)) {\n const st: Stencil[] = [];\n for (const raw of r.stencils as Stencil[]) {\n try {\n st.push(normalizeStencil(raw, typeof raw?.id === 'string' ? raw.id : 'stencil'));\n } catch {\n // an unreadable stencil is dropped\n }\n }\n if (st.length) out.stencils = st;\n }\n const paint = readPaintSource(r.paint);\n if (paint && paint.type === 'gradient') out.paint = paint;\n if (Array.isArray(r.steps) && r.steps.length === points.length && r.steps.every((v) => Number.isInteger(v) && v >= 0)) out.steps = [...(r.steps as number[])];\n return out;\n}\n\n/** The bounding box of a stroke's points. */\nexport function strokeBounds(points: readonly PenPoint[]): { x: number; y: number; width: number; height: number } {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const p of points) {\n if (p.x < x0) x0 = p.x;\n if (p.y < y0) y0 = p.y;\n if (p.x > x1) x1 = p.x;\n if (p.y > y1) y1 = p.y;\n }\n return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };\n}\n\nexport interface StrokeVariation {\n /** Move by this many document pixels (default 0). */\n dx?: number;\n dy?: number;\n /** Scale about the origin (default 1); brush size scales with it. */\n scale?: number;\n /** Rotate about the origin, degrees clockwise (default 0). */\n rotate?: number;\n /** Mirror horizontally about the origin (default false). */\n flipX?: boolean;\n /** The origin for scale, rotate and flip (default: the centre of the strokes' bounds). */\n origin?: { x: number; y: number };\n /** Multiply every pressure by this (default 1). */\n pressureScale?: number;\n}\n\n/** The affine map of a variation about `origin`. */\nexport function variationAffine(v: StrokeVariation, origin: { x: number; y: number }): Affine {\n const s = v.scale ?? 1;\n const a = ((v.rotate ?? 0) * Math.PI) / 180;\n const cos = Math.cos(a) * s;\n const sin = Math.sin(a) * s;\n const fx = v.flipX ? -1 : 1;\n // Translate to the origin, flip, rotate (clockwise with y down) and scale, translate back, move.\n const linear: Affine = [cos * fx, -sin, sin * fx, cos, 0, 0];\n const toOrigin: Affine = [1, 0, 0, 1, -origin.x, -origin.y];\n const back: Affine = [1, 0, 0, 1, origin.x + (v.dx ?? 0), origin.y + (v.dy ?? 0)];\n return composeAffine(back, composeAffine(linear, toOrigin));\n}\n\n/** A stroke varied by `m` (points mapped; symmetry copies conjugated so they stay symmetric). */\nexport function varyStroke(stroke: RecordedStroke, m: Affine, v: StrokeVariation): RecordedStroke {\n const s = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));\n const ps = v.pressureScale ?? 1;\n const points = stroke.points.map((p) => {\n const q = mapPenPoint(m, p);\n if (ps !== 1) q.pressure = Math.min(1, Math.max(0, (p.pressure ?? 1) * ps));\n return q;\n });\n const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;\n const out: RecordedStroke = { ...stroke, brush, points };\n if (stroke.paint && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);\n if (stroke.copies?.length) {\n const inv = invertAffine(m);\n out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));\n }\n return out;\n}\n","/**\n * Compositing for the paint editor, on the shared `RasterCompositor`: the paper is a content layer\n * at the bottom of the stack (its tiles are generated from the paper settings), and view-only\n * layers (the tracing image) sit between it and the paint. No compositor of our own.\n */\n\nimport { RasterCompositor, type CompositorLayer, type CompositorProfile, type RasterSources } from '../raster/compositor';\nimport type { RasterRect } from '../raster/surface';\nimport type { PaintLayer } from './document';\nimport { PaperGrain, renderPaper, type PaperSettings } from './paper';\n\n/** The paper as a layer of the compositor (generated, never stored). */\nexport interface PaperNode extends CompositorLayer {\n type: 'paper';\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: 'normal';\n paper: PaperSettings;\n /** The light, for metallic paper. */\n light?: { azimuth: number; elevation: number };\n}\n\n/** A view-only RGBA image under the paint (the tracing image), drawn at its opacity. */\nexport interface ImageNode extends CompositorLayer {\n type: 'image';\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: 'normal';\n /** Canvas-sized RGBA. */\n image: { width: number; height: number; data: Uint8ClampedArray };\n /** Changes when the pixels change. */\n version: number;\n}\n\nexport type ViewLayer = PaintLayer | PaperNode | ImageNode;\n\nexport const PAPER_NODE_ID = '__paper';\nexport const TRACING_NODE_ID = '__tracing';\n\nexport function paperKey(p: PaperSettings, light?: { azimuth: number; elevation: number }): string {\n const metal = p.metallic ?? 0;\n return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry, metal, metal > 0 && light ? [light.azimuth, light.elevation] : null]);\n}\n\nconst paperNodes = new WeakMap<PaperSettings, PaperNode>();\n\n/** The paper as a compositor layer (one object per paper settings object and light, so caches hold). */\nexport function paperNode(paper: PaperSettings, light?: { azimuth: number; elevation: number }): PaperNode {\n let n = paperNodes.get(paper);\n const metal = (paper.metallic ?? 0) > 0;\n if (!n || (metal && (n.light?.azimuth !== light?.azimuth || n.light?.elevation !== light?.elevation))) {\n n = { type: 'paper', id: PAPER_NODE_ID, name: 'Paper', visible: true, opacity: 1, blend: 'normal', paper, ...(metal && light ? { light: { azimuth: light.azimuth, elevation: light.elevation } } : {}) };\n paperNodes.set(paper, n);\n }\n return n;\n}\n\nconst grainCache = new Map<string, PaperGrain>();\nconst tileCache = new Map<string, Uint8ClampedArray>();\n\n/** The paper's dry height field, built once per settings. */\nexport function paperGrain(paper: PaperSettings): PaperGrain {\n const key = JSON.stringify([paper.pattern, paper.seed, paper.scale, paper.tiling, paper.dry]);\n let g = grainCache.get(key);\n if (!g) {\n g = new PaperGrain(paper, 'dry');\n if (grainCache.size > 16) grainCache.delete(grainCache.keys().next().value as string);\n grainCache.set(key, g);\n }\n return g;\n}\n\nfunction paperTile(paper: PaperSettings, r: RasterRect, light?: { azimuth: number; elevation: number }): Uint8ClampedArray {\n const key = `${paperKey(paper, light)}|${r.x},${r.y},${r.width},${r.height}`;\n let t = tileCache.get(key);\n if (!t) {\n t = renderPaper(paper, r, paperGrain(paper), light);\n if (tileCache.size > 512) tileCache.delete(tileCache.keys().next().value as string);\n tileCache.set(key, t);\n }\n return t;\n}\n\nconst PROFILE: CompositorProfile<ViewLayer, RasterSources> = {\n contentTile(layer, r) {\n if (layer.type === 'paper') return paperTile((layer as PaperNode).paper, r, (layer as PaperNode).light);\n if (layer.type === 'image') {\n const { image } = layer as ImageNode;\n const out = new Uint8ClampedArray(r.width * r.height * 4);\n const x0 = Math.max(0, r.x);\n const y0 = Math.max(0, r.y);\n const x1 = Math.min(image.width, r.x + r.width);\n const y1 = Math.min(image.height, r.y + r.height);\n if (x1 <= x0 || y1 <= y0) return null;\n for (let y = y0; y < y1; y++) out.set(image.data.subarray((y * image.width + x0) * 4, (y * image.width + x1) * 4), ((y - r.y) * r.width + (x0 - r.x)) * 4);\n return out;\n }\n return null;\n },\n layerKey(layer) {\n if (layer.type === 'paper') return paperKey((layer as PaperNode).paper, (layer as PaperNode).light);\n if (layer.type === 'image') return (layer as ImageNode).version;\n return undefined;\n },\n};\n\n/** A compositor for paint editor stacks (paper and view-only images included). */\nexport function createPaintCompositor(): RasterCompositor<ViewLayer, RasterSources> {\n return new RasterCompositor<ViewLayer, RasterSources>(PROFILE);\n}\n\n/**\n * Area-averaged downscale of straight-alpha RGBA (colours weighted by alpha). Sizes at or above\n * the source copy it.\n */\nexport function downsampleRgba(data: ArrayLike<number>, width: number, height: number, tw: number, th: number): Uint8ClampedArray {\n tw = Math.max(1, Math.round(tw));\n th = Math.max(1, Math.round(th));\n if (tw >= width && th >= height) return Uint8ClampedArray.from(data as ArrayLike<number>);\n const out = new Uint8ClampedArray(tw * th * 4);\n const sx = width / tw;\n const sy = height / th;\n for (let y = 0; y < th; y++) {\n const y0 = y * sy;\n const y1 = y0 + sy;\n for (let x = 0; x < tw; x++) {\n const x0 = x * sx;\n const x1 = x0 + sx;\n let r = 0;\n let g = 0;\n let b = 0;\n let a = 0;\n let w = 0;\n for (let yy = Math.floor(y0); yy < Math.ceil(y1) && yy < height; yy++) {\n const wy = Math.min(y1, yy + 1) - Math.max(y0, yy);\n for (let xx = Math.floor(x0); xx < Math.ceil(x1) && xx < width; xx++) {\n const wgt = wy * (Math.min(x1, xx + 1) - Math.max(x0, xx));\n const i = (yy * width + xx) * 4;\n const al = data[i + 3] * wgt;\n r += data[i] * al;\n g += data[i + 1] * al;\n b += data[i + 2] * al;\n a += al;\n w += wgt;\n }\n }\n const o = (y * tw + x) * 4;\n if (a > 0) {\n out[o] = r / a;\n out[o + 1] = g / a;\n out[o + 2] = b / a;\n }\n out[o + 3] = w > 0 ? a / w : 0;\n }\n }\n return out;\n}\n","/**\n * Saved paint documents: JSON plus PNG assets. Layers go through the image editor's layer\n * serializer (`serializeLayers` / `deserializeLayers`), so pixel layers and groups are stored the\n * same way in both editors, including external asset stores. The paper is plain settings: it is\n * regenerated on load, never stored as pixels.\n *\n * Forward compatibility: readers keep fields they do not know at the top level (`extensions`) and\n * write them back, so a later version can add per-layer wet state, height or material channels\n * as new assets keyed by layer id without breaking older readers (see `PaintDocumentExtensions`).\n */\n\nimport {\n deserializeLayers,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayer,\n} from '../image-editor/layers/serialize';\nimport type { ImageLayer } from '../image-editor/layers/model';\nimport { findLayer, flattenLayers } from '../raster/layers';\nimport { normalizeBrushStroke, type RecordedStroke } from './strokes';\nimport { normalizeCanvas, type PaintDocumentState, type PaintEvent, type PaintLayer } from './document';\nimport { normalizePaper, type PaperSettings } from './paper';\nimport type { WetEvent } from './wet';\n\nexport const PAINT_FORMAT = 'pixerate-paint' as const;\nexport const PAINT_SCHEMA_VERSION = 1 as const;\n\n/** Where a host keeps layer PNGs outside the JSON (the image editor's asset store contract). */\nexport type PaintAssetStore = ImageEditorAssetStore;\n\n/**\n * Fields a later version adds. Phase 1 writes none of them and keeps any it reads. Planned:\n * `layerChannels: { [layerId]: { height?: asset, material?: asset } }` for impasto and\n * metallic paint. Wet paint itself is never saved (saving dries it); its settings are (`wet`).\n */\nexport type PaintDocumentExtensions = Record<string, unknown>;\n\nexport interface SerializedPaintDocument {\n format: typeof PAINT_FORMAT;\n version: typeof PAINT_SCHEMA_VERSION;\n width: number;\n height: number;\n dpi: number;\n paper: PaperSettings;\n /** Bottom to top: pixel layers and groups, as the image editor stores them. */\n layers: SerializedLayer[];\n /** Asset id → PNG data URL (or an external reference). */\n assets: Record<string, string>;\n activeLayerId: string | null;\n /** Recorded strokes (strokes are data), oldest first; missing when saved without them. */\n strokes?: RecordedStroke[];\n /** Everything else (document-level state of later phases), written back as read. */\n [extension: string]: unknown;\n}\n\nconst KNOWN = new Set(['format', 'version', 'width', 'height', 'dpi', 'paper', 'layers', 'assets', 'activeLayerId', 'strokes', 'guides', 'swatches', 'mixingPad', 'wet', 'wetEvents', 'paintEvents']);\n\n/** Fields of a saved document this version does not read, kept for writing back. */\nexport function documentExtensions(raw: Record<string, unknown>): PaintDocumentExtensions {\n const out: PaintDocumentExtensions = {};\n for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;\n return out;\n}\n\nexport interface SerializeOptions {\n /** Include the recorded strokes (default true). */\n strokes?: boolean;\n /** Later-phase fields to write as they were read. */\n extensions?: PaintDocumentExtensions;\n /** Painting guides (symmetry, perspective, tracing and reference images as PNG data URLs). */\n guides?: unknown;\n swatches?: unknown;\n /** The mixing pad: `{ current?: png data URL, saved: [{ id, name, png }] }`. */\n mixingPad?: unknown;\n /** Wet media settings: `{ tilt, layers: { [layerId]: { params, flow } } }`. */\n wet?: unknown;\n}\n\nfunction header(state: PaintDocumentState, options: SerializeOptions): Omit<SerializedPaintDocument, 'layers' | 'assets'> {\n const out: Omit<SerializedPaintDocument, 'layers' | 'assets'> = {\n ...(options.extensions ?? {}),\n format: PAINT_FORMAT,\n version: PAINT_SCHEMA_VERSION,\n width: state.canvas.width,\n height: state.canvas.height,\n dpi: state.canvas.dpi,\n paper: { ...state.paper, dry: { ...state.paper.dry }, wet: { ...state.paper.wet } },\n activeLayerId: state.activeLayerId,\n };\n if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));\n if (options.guides !== undefined) out.guides = JSON.parse(JSON.stringify(options.guides));\n if (options.swatches !== undefined) out.swatches = JSON.parse(JSON.stringify(options.swatches));\n if (options.mixingPad !== undefined) (out as Record<string, unknown>).mixingPad = JSON.parse(JSON.stringify(options.mixingPad));\n if (options.wet !== undefined) (out as Record<string, unknown>).wet = JSON.parse(JSON.stringify(options.wet));\n if (state.wetEvents?.length) (out as Record<string, unknown>).wetEvents = JSON.parse(JSON.stringify(state.wetEvents));\n if (state.paintEvents?.length) (out as Record<string, unknown>).paintEvents = JSON.parse(JSON.stringify(state.paintEvents));\n return out;\n}\n\n/** The document as JSON with inline PNG data URLs. */\nexport function serializePaintDocument(state: PaintDocumentState, options: SerializeOptions = {}): SerializedPaintDocument {\n const { layers, assets } = serializeLayers(state.layers as unknown as ImageLayer[]);\n return { ...header(state, options), layers, assets } as SerializedPaintDocument;\n}\n\n/** The document as JSON with each PNG uploaded to `store` once (by content). */\nexport async function serializePaintDocumentExternal(state: PaintDocumentState, store: PaintAssetStore, options: SerializeOptions = {}): Promise<SerializedPaintDocument> {\n const { layers, assets } = await serializeLayersExternal(state.layers as unknown as ImageLayer[], store);\n return { ...header(state, options), layers, assets } as SerializedPaintDocument;\n}\n\nexport interface LoadedPaintDocument {\n state: PaintDocumentState;\n extensions: PaintDocumentExtensions;\n /** The saved guides and swatches, as stored (the controller validates them). */\n guides?: unknown;\n swatches?: unknown;\n /** The saved mixing pad, as stored (the controller validates it). */\n mixingPad?: unknown;\n /** The saved wet settings, as stored (the controller validates them). */\n wet?: unknown;\n /** What could not be read (the rest still loads). */\n problems: string[];\n}\n\n/** Keeps pixel layers and groups (the paint editor's kinds); reports and drops the rest. */\nfunction paintLayersOnly(layers: ImageLayer[], problems: string[]): PaintLayer[] {\n const out: PaintLayer[] = [];\n for (const l of layers) {\n if (l.type === 'pixel') {\n const { filters, source, ...rest } = l;\n if (filters?.length) problems.push(`Layer \"${l.name}\": its filters are an image editor feature and were dropped.`);\n if (source) problems.push(`Layer \"${l.name}\" refers to an image editor source image; it loads empty.`);\n out.push(rest as PaintLayer);\n } else if (l.type === 'group') out.push({ ...l, children: paintLayersOnly(l.children, problems) } as PaintLayer);\n else problems.push(`Layer \"${l.name}\" is an image editor ${l.type} layer, which the paint editor does not have; it was left out.`);\n }\n return out;\n}\n\nfunction readStrokes(raw: unknown, problems: string[]): RecordedStroke[] {\n if (!Array.isArray(raw)) return [];\n const out: RecordedStroke[] = [];\n raw.forEach((s, i) => {\n const stroke = normalizeBrushStroke(s);\n if (stroke) out.push(stroke);\n else problems.push(`strokes[${i}] is not a recorded stroke; skipped.`);\n });\n return out;\n}\n\n/** Reads a saved document with inline assets. Throws when it is not a paint document at all. */\nexport function deserializePaintDocument(raw: unknown): LoadedPaintDocument {\n if (!raw || typeof raw !== 'object') throw new Error('Not a paint document: expected a JSON object.');\n const r = raw as Record<string, unknown>;\n if (r.format !== PAINT_FORMAT) throw new Error(`Not a paint document: format is ${JSON.stringify(r.format)}, expected \"${PAINT_FORMAT}\".`);\n if (typeof r.version !== 'number' || r.version > PAINT_SCHEMA_VERSION) {\n throw new Error(`Paint document version ${JSON.stringify(r.version)} is newer than this editor reads (${PAINT_SCHEMA_VERSION}); update @pixerate/editor.`);\n }\n const problems: string[] = [];\n const canvas = normalizeCanvas({ width: r.width as number, height: r.height as number, dpi: (r.dpi as number | undefined) ?? undefined });\n const paper = normalizePaper((r.paper as Record<string, unknown>) ?? {});\n const assets = r.assets && typeof r.assets === 'object' ? (r.assets as Record<string, string>) : {};\n const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);\n const wanted = typeof r.activeLayerId === 'string' ? r.activeLayerId : null;\n const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : (flattenLayers(layers).filter((l) => l.type === 'pixel').pop()?.id ?? null);\n const wetEvents = Array.isArray(r.wetEvents) ? (r.wetEvents as WetEvent[]).filter((e) => e && typeof e === 'object' && Number.isInteger(e.clock) && typeof e.kind === 'string') : [];\n const state: PaintDocumentState = { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) };\n if (wetEvents.length) state.wetEvents = wetEvents;\n const paintEvents = Array.isArray(r.paintEvents) ? (r.paintEvents as PaintEvent[]).filter((e) => e && typeof e === 'object' && Number.isInteger(e.at) && typeof e.layerId === 'string' && ['fill', 'clear', 'flatten', 'stickers'].includes(e.kind)) : [];\n if (paintEvents.length) state.paintEvents = paintEvents;\n return { state, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, mixingPad: r.mixingPad, wet: r.wet, problems };\n}\n\n/** Reads a saved document whose assets may be external references in `store`. */\nexport async function deserializePaintDocumentAsync(raw: unknown, store?: PaintAssetStore): Promise<LoadedPaintDocument> {\n if (!raw || typeof raw !== 'object') throw new Error('Not a paint document: expected a JSON object.');\n const r = raw as Record<string, unknown>;\n const problems: string[] = [];\n const assets = await resolveExternalAssets((r.assets as Record<string, string>) ?? {}, store, problems);\n const loaded = deserializePaintDocument({ ...r, assets });\n return { ...loaded, problems: [...problems, ...loaded.problems] };\n}\n","/**\n * The canvas view: zoom, pan, rotation and mirroring. View state only: the pixels never resample,\n * the view maps document pixels to the screen and pointer positions back.\n */\n\nimport type { Affine } from './strokes';\nimport { applyAffine, invertAffine } from './strokes';\n\nexport interface PaintView {\n /** Screen pixels per document pixel (0.02..64, default 1). */\n zoom: number;\n /** Offset of the canvas centre from the viewport centre, in screen pixels. */\n pan: { x: number; y: number };\n /** Rotation in degrees clockwise, -180..180 (default 0). */\n rotation: number;\n /** Mirror the view left to right (default false). */\n mirror: boolean;\n}\n\nexport const MIN_ZOOM = 0.02;\nexport const MAX_ZOOM = 64;\n\nexport const DEFAULT_VIEW: Readonly<PaintView> = { zoom: 1, pan: { x: 0, y: 0 }, rotation: 0, mirror: false };\n\n/** Degrees into -180..180. */\nexport function normalizeRotation(deg: number): number {\n if (!Number.isFinite(deg)) return 0;\n let r = deg % 360;\n if (r > 180) r -= 360;\n if (r <= -180) r += 360;\n return Math.round(r * 1000) / 1000;\n}\n\nexport function normalizeView(raw: Partial<PaintView>, base: PaintView = DEFAULT_VIEW): PaintView {\n const zoom = typeof raw.zoom === 'number' && Number.isFinite(raw.zoom) ? Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, raw.zoom)) : base.zoom;\n const pan = raw.pan && Number.isFinite(raw.pan.x) && Number.isFinite(raw.pan.y) ? { x: raw.pan.x, y: raw.pan.y } : { ...base.pan };\n return {\n zoom,\n pan,\n rotation: typeof raw.rotation === 'number' ? normalizeRotation(raw.rotation) : base.rotation,\n mirror: typeof raw.mirror === 'boolean' ? raw.mirror : base.mirror,\n };\n}\n\n/**\n * Document → screen for a canvas of `canvas` size shown in a viewport of `viewport` size:\n * the canvas centre goes to the viewport centre plus `pan`, then zoom, rotation and mirroring\n * apply about it.\n */\nexport function viewMatrix(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }): Affine {\n const a = (view.rotation * Math.PI) / 180;\n const z = view.zoom;\n const m = view.mirror ? -1 : 1;\n const cos = Math.cos(a) * z;\n const sin = Math.sin(a) * z;\n // Screen = C + R(θ) · M · z · (p − c), with y down (clockwise rotation).\n const cx = canvas.width / 2;\n const cy = canvas.height / 2;\n const sx = viewport.width / 2 + view.pan.x;\n const sy = viewport.height / 2 + view.pan.y;\n const A = cos * m;\n const B = -sin;\n const C = sin * m;\n const D = cos;\n return [A, B, C, D, sx - (A * cx + B * cy), sy - (C * cx + D * cy)];\n}\n\n/** Screen position → document position. */\nexport function screenToDocument(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }, x: number, y: number): { x: number; y: number } {\n return applyAffine(invertAffine(viewMatrix(view, canvas, viewport)), x, y);\n}\n\n/** Document position → screen position. */\nexport function documentToScreen(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }, x: number, y: number): { x: number; y: number } {\n return applyAffine(viewMatrix(view, canvas, viewport), x, y);\n}\n\n/** A CSS `matrix(...)` for an element of the canvas size placed at the viewport's top-left. */\nexport function viewCssTransform(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }): string {\n const m = viewMatrix(view, canvas, viewport);\n const f = (v: number) => Math.round(v * 10000) / 10000;\n return `matrix(${f(m[0])}, ${f(m[2])}, ${f(m[1])}, ${f(m[3])}, ${f(m[4])}, ${f(m[5])})`;\n}\n\n/** The zoom that fits the canvas in the viewport (with `margin` screen pixels around it). */\nexport function fitZoom(canvas: { width: number; height: number }, viewport: { width: number; height: number }, margin = 24): number {\n const w = Math.max(1, viewport.width - 2 * margin);\n const h = Math.max(1, viewport.height - 2 * margin);\n return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min(w / canvas.width, h / canvas.height)));\n}\n\n/**\n * Pen rotation as the document sees it: a rotated or mirrored view turns the pen's azimuth and\n * barrel rotation too, so a tilted pencil shades the same way on screen however the canvas is turned.\n */\nexport function viewPenToDocument<P extends { azimuth?: number; twist?: number }>(view: PaintView, p: P): P {\n if (!view.rotation && !view.mirror) return p;\n const out = { ...p };\n const r = (view.rotation * Math.PI) / 180;\n if (typeof p.azimuth === 'number') {\n let a = p.azimuth - r;\n if (view.mirror) a = Math.PI - a;\n out.azimuth = ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);\n }\n if (typeof p.twist === 'number') {\n let t = p.twist - view.rotation;\n if (view.mirror) t = -t;\n out.twist = ((t % 360) + 360) % 360;\n }\n return out;\n}\n","/**\n * Pen input helpers for the bindings: Pointer Events (structurally typed, no DOM import) into\n * `PenPoint`s, palm rejection by hand, and which hand the user draws with. The bindings feed\n * coalesced events into the stroke and draw predicted ones as a throwaway preview.\n */\n\nimport { penOrientation, type PenPoint } from './engine';\nimport { viewPenToDocument, type PaintView } from './view';\nimport { pointerPressure } from '../paint/pointer';\n\n/** The Pointer Event fields the paint editor reads. */\nexport interface PointerSample {\n pointerType?: string;\n pressure?: number;\n tiltX?: number;\n tiltY?: number;\n /** Radians (Pointer Events Level 3). */\n altitudeAngle?: number;\n azimuthAngle?: number;\n /** Degrees 0..359. */\n twist?: number;\n timeStamp?: number;\n}\n\n/** Which hand holds the pen: palm rejection ignores touches on that side of it. */\nexport type Hand = 'right' | 'left';\n\n/**\n * Pressure of a pointer sample: pens report it; mice report 0.5 while pressed and pens 0 on\n * `pointerup`, both of which mean \"use full\" (see GOTCHAS). The shared `pointerPressure`.\n */\nexport function samplePressure(e: PointerSample): number {\n return pointerPressure(e);\n}\n\n/**\n * A pointer sample at document position (x, y) as a pen point. Only pens report tilt and twist;\n * a pen's lean and barrel rotation are turned with the view, so marks look the same on screen\n * whatever the canvas rotation and mirroring.\n */\nexport function pointerToPenPoint(e: PointerSample, x: number, y: number, view?: PaintView): PenPoint {\n const p: PenPoint = { x, y, pressure: samplePressure(e) };\n if (typeof e.timeStamp === 'number' && Number.isFinite(e.timeStamp)) p.t = e.timeStamp;\n if (e.pointerType !== 'pen') return p;\n const o = penOrientation({\n x,\n y,\n altitude: typeof e.altitudeAngle === 'number' ? e.altitudeAngle : undefined,\n azimuth: typeof e.azimuthAngle === 'number' ? e.azimuthAngle : undefined,\n tiltX: e.tiltX,\n tiltY: e.tiltY,\n });\n // A pen standing straight up with no tilt reported keeps no orientation (most pens without tilt report 0, 0).\n if (o && o.altitude < Math.PI / 2 - 1e-6) {\n p.altitude = o.altitude;\n p.azimuth = o.azimuth;\n }\n if (typeof e.twist === 'number' && Number.isFinite(e.twist) && e.twist !== 0) p.twist = e.twist;\n return view ? viewPenToDocument(view, p) : p;\n}\n\n/**\n * Palm rejection: whether a touch at `touch` (screen pixels) is probably the drawing hand resting\n * on the screen while the pen is at `pen`. A right hand rests below and to the right of the pen, a\n * left hand below and to the left.\n */\nexport function isPalmTouch(touch: { x: number; y: number }, pen: { x: number; y: number } | null, hand: Hand): boolean {\n if (!pen) return false;\n const dx = touch.x - pen.x;\n const dy = touch.y - pen.y;\n const side = hand === 'right' ? dx : -dx;\n return dy > -40 && side > -40 && Math.hypot(dx, dy) < 600;\n}\n","/**\n * JPEG and WebP encoding. PNG is encoded in pure TypeScript (`encodePng`); the lossy formats need\n * the platform's codecs, so they go through an `OffscreenCanvas` (browsers and Workers) or a host\n * encoder. Nothing is imported: the globals are looked up at call time.\n */\n\nexport type ImageFormat = 'image/png' | 'image/jpeg' | 'image/webp';\n\n/** Encodes straight-alpha RGBA as `format` at `quality` (0..1). */\nexport type ImageEncoder = (image: { width: number; height: number; data: Uint8ClampedArray }, format: 'image/jpeg' | 'image/webp', quality: number) => Promise<Uint8Array>;\n\ninterface CanvasLike {\n getContext(type: '2d'): { putImageData(data: unknown, x: number, y: number): void } | null;\n convertToBlob(options: { type: string; quality: number }): Promise<{ arrayBuffer(): Promise<ArrayBuffer>; type: string }>;\n}\n\n/** An encoder through `OffscreenCanvas`, or `null` where there is none (Node, tests). */\nexport function defaultImageEncoder(): ImageEncoder | null {\n const g = globalThis as unknown as {\n OffscreenCanvas?: new (w: number, h: number) => CanvasLike;\n ImageData?: new (data: Uint8ClampedArray, w: number, h: number) => unknown;\n };\n if (typeof g.OffscreenCanvas !== 'function' || typeof g.ImageData !== 'function') return null;\n const Canvas = g.OffscreenCanvas;\n const Data = g.ImageData;\n return async (image, format, quality) => {\n const canvas = new Canvas(image.width, image.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('No 2D context to encode with.');\n ctx.putImageData(new Data(new Uint8ClampedArray(image.data), image.width, image.height), 0, 0);\n const blob = await canvas.convertToBlob({ type: format, quality });\n if (blob.type !== format) throw new Error(`This browser cannot encode ${format} (it produced ${blob.type || 'nothing'}); export PNG instead.`);\n return new Uint8Array(await blob.arrayBuffer());\n };\n}\n\n/**\n * Decodes an image file (PNG, JPEG, WebP, … whatever the browser reads) into RGBA pixels through\n * `createImageBitmap` and an `OffscreenCanvas`. Browser and Worker only.\n */\nexport async function decodeImageBlob(blob: Blob): Promise<{ width: number; height: number; data: Uint8ClampedArray }> {\n const g = globalThis as unknown as {\n createImageBitmap?: (b: Blob) => Promise<{ width: number; height: number; close?(): void }>;\n OffscreenCanvas?: new (w: number, h: number) => { getContext(type: '2d'): { drawImage(img: unknown, x: number, y: number): void; getImageData(x: number, y: number, w: number, h: number): { data: Uint8ClampedArray } } | null };\n };\n if (typeof g.createImageBitmap !== 'function' || typeof g.OffscreenCanvas !== 'function') throw new Error('Decoding images needs a browser (createImageBitmap and OffscreenCanvas).');\n const bitmap = await g.createImageBitmap(blob);\n const canvas = new g.OffscreenCanvas(bitmap.width, bitmap.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('No 2D context to decode with.');\n ctx.drawImage(bitmap, 0, 0);\n const data = new Uint8ClampedArray(ctx.getImageData(0, 0, bitmap.width, bitmap.height).data);\n bitmap.close?.();\n return { width: bitmap.width, height: bitmap.height, data };\n}\n\n/** Saves bytes as a file through a temporary link. Browser only. */\nexport function downloadBytes(bytes: Uint8Array | string, fileName: string, mimeType: string): void {\n const g = globalThis as unknown as {\n document?: { createElement(tag: 'a'): { href: string; download: string; click(): void } };\n URL?: { createObjectURL(b: Blob): string; revokeObjectURL(u: string): void };\n Blob?: new (parts: unknown[], options: { type: string }) => Blob;\n };\n if (!g.document || !g.URL || !g.Blob) throw new Error('Downloading needs a browser.');\n const url = g.URL.createObjectURL(new g.Blob([bytes], { type: mimeType }));\n const a = g.document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => g.URL!.revokeObjectURL(url), 1000);\n}\n","/**\n * Wet media in the paint editor (watercolour first): a layer painted with a wet brush gets a wet\n * layer in the document's `WetWorld` (`@pixerate/editor/paint`), and its pixels are the world's\n * display of it until the paint dries.\n *\n * - **Wet brushes** (`engine: 'wet'`) run through `WetStrokeDriver`: the same pen input as dry\n * media (`PenDabWalker`: pressure, tilt, rotation, jitter, symmetry copies), each dab's tip\n * coverage (quantised to 1/4096, so tiny floating-point differences between engines never reach\n * the simulation) applied by the brush's wet tool. `paint` goes through the shared pick-up / mix\n * exchange over a `WetMedium`, so a wet brush lifts and mixes wet paint like the phase 2 brushes.\n * - **Time** is the world's step clock. The host calls `tick` every frame (the bindings do); it\n * steps at `speed` steps per second unless paused. Agents call `advance` or `dry`.\n * - **Dabs between steps.** Edits only happen between batches: points pushed while a batch runs\n * (in Web Workers) wait for it, and every raw point records the clock it was applied at\n * (`RecordedStroke.steps`), so a stroke replays exactly.\n * - **Drying.** Tiles that have dried are baked into the layer's pixels by the world; when a whole\n * layer is dry its wet layer is dropped and it is an ordinary pixel layer again. `dry` dries at\n * once. Saving and exporting use the deterministic bake of everything still wet (\"wet paint is\n * dried on save\": the document holds dried pixels, never wet state).\n * - **Undo.** Each wet step of history keeps a snapshot of the world before and after it, within\n * `undoMemory` bytes; older snapshots are dropped, and undoing past them leaves that paint dry\n * as it looked.\n */\n\nimport { dabRect, rasterizeDab } from '../paint/brush';\nimport { exchangeRates, PaintExchange, type MixerLoad, type RgbTriple } from '../paint/exchange';\nimport { pigmentOf, PIGMENT_UNIT } from '../paint/wet/film';\nimport { DEFAULT_FLOW, flowField, normalizeFlow, type WetFlowSpec } from '../paint/wet/flow';\nimport { WetMedium } from '../paint/wet/medium';\nimport { PaintGradientSampler, type PaintGradientSample, type PaintGradientSource } from '../paint/gradient-paint';\nimport { DEFAULT_WET_PARAMS, FLAT, normalizeTilt, normalizeWetParams, type WetParams, type WetTilt } from '../paint/wet/model';\nimport { createWetGpuEngine, findWebGpu, type WetGpuEngine, type WetGpuProvider, type WetGpuSupport } from '../paint/wet/gpu';\nimport { WetWorld, type WetBackendInfo, type WetLaneStats, type WetPaperInput, type WetSnapshot, type WetWorldOptions } from '../paint/wet/world';\nimport { TILE, TiledSurface, type RasterRect } from '../raster/surface';\nimport type { PaintBrush, WetMediumKind } from './brushes';\nimport { IDENTITY_COPY, PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine';\nimport { grainCatch, hexToRgb8, PaperGrain, type PaperGrain as PaperGrainType, type PaperSettings } from './paper';\n\n/** Water depth a stroke lays at the tip's centre at `water` 1 (the sum over its dabs). */\nconst STROKE_WATER = 0.9;\n/** Pigment mass a stroke lays at the tip's centre at `pigment` 1: a little more than a full-strength wash. */\nconst STROKE_MASS = PIGMENT_UNIT * 2.5;\n/** Coverage quantum: dab coverage is rounded to this before it reaches the simulation. */\nconst COVERAGE_STEPS = 4096;\n\nfunction union(a: RasterRect | null, b: RasterRect): RasterRect {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\n/** The paper as the wet simulation reads it. */\nexport function wetPaperInput(paper: PaperSettings): WetPaperInput {\n const grain = new PaperGrain(paper, 'wet');\n const flat = grain.flat;\n return {\n absorption: paper.absorption,\n permeability: paper.permeability,\n roughness: flat ? 0 : paper.roughness,\n color: hexToRgb8(paper.color),\n heightAt: flat ? () => 0.5 : (x, y) => grain.at(x, y),\n };\n}\n\nexport interface WetStrokeInit {\n world: WetWorld;\n laneId: string;\n brush: PaintBrush;\n /** Paint colour, sRGB 0..1. */\n color: RgbTriple;\n load: MixerLoad;\n grain: PaperGrainType | null;\n paperDepth: number;\n paperColor: readonly [number, number, number];\n seed: number;\n copies: readonly StrokeCopy[];\n /** Where paint may land (canvas-sized coverage 0..255: the stencils' mask), or none. */\n mask?: { width: number; height: number; data: ArrayLike<number> } | null;\n /** Gradient paint (resolved): each dab's pigment comes from it. */\n paint?: PaintGradientSource | null;\n}\n\ninterface CopyState {\n walker: PenDabWalker;\n exchange: PaintExchange | null;\n last: PenDab | null;\n}\n\n/**\n * One wet-brush stroke: pen samples → dabs → wet tool edits. Points are applied only while the\n * world is between batches (`flush` applies the ones that waited).\n */\nexport class WetStrokeDriver {\n dabCount = 0;\n bounds: RasterRect | null = null;\n /** The world's clock at which each raw point was applied (recorded with the stroke). */\n readonly steps: number[] = [];\n private readonly smoother: PenSmoother;\n private readonly states: CopyState[];\n private readonly medium: WetMedium;\n private readonly queue: PenPoint[] = [];\n private finishing = false;\n private finished = false;\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n private tip = new Float32Array(0);\n private cov = new Float32Array(0);\n private readonly paint: RgbTriple = [0, 0, 0];\n private readonly depth: number;\n\n constructor(readonly init: WetStrokeInit) {\n const brush = init.brush;\n this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);\n const copies = init.copies.length ? init.copies : [IDENTITY_COPY];\n const wet = brush.wet;\n const spacing = Math.max(0.02, brush.tip.spacing);\n // A stroke lays `STROKE_WATER × water` at the tip's centre: each dab a share by its spacing.\n const perDab = Math.min(1, spacing * 1.2);\n // Thinner: more water, less pigment and body.\n const thin = wet.thinner;\n this.medium = new WetMedium(init.world, init.laneId, {\n water: STROKE_WATER * Math.min(1, wet.water * (1 + thin)) * perDab,\n mass: STROKE_MASS * wet.pigment * (1 - 0.6 * thin) * perDab,\n body: wet.body * (1 - thin),\n });\n // Loading: below 1 the reservoir drains along the stroke, so the paint runs out.\n const rates = exchangeRates(brush.behavior.pickup, brush.behavior.mix, wet.load, brush.behavior.mixing);\n this.states = copies.map((c, i) => ({\n walker: new PenDabWalker(brush, (init.seed + i * 7919) | 0, c.linear),\n exchange: wet.tool === 'paint' ? new PaintExchange(rates, init.load) : null,\n last: null,\n }));\n this.copies = copies;\n this.depth = init.grain && !init.grain.flat ? Math.min(1, Math.max(0, init.paperDepth * brush.grain)) : 0;\n // Across the tip makes no sense for one wet dab: the colour follows the dab instead.\n const g = init.paint ?? null;\n this.sampler = g ? new PaintGradientSampler(g.map.by === 'across-tip' ? { ...g, map: { by: 'distance', length: 300, repeat: 'mirror' } } : g, init.seed) : null;\n }\n\n private readonly sampler: PaintGradientSampler | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n\n private readonly copies: readonly StrokeCopy[];\n\n /** What copy 0's brush carries after the dabs so far. */\n get load(): MixerLoad | null {\n return this.states[0].exchange?.load ?? null;\n }\n\n /** Whether every point (and the end) has been applied. */\n get done(): boolean {\n return this.finished;\n }\n\n /** Adds raw points; applies them now unless a batch is running. Returns where pixels changed. */\n push(points: readonly PenPoint[]): RasterRect[] {\n for (const p of points) this.queue.push(p);\n return this.flush();\n }\n\n /** Asks for the end; it happens now unless a batch is running (then at the next `flush`). */\n finish(): RasterRect[] {\n this.finishing = true;\n return this.flush();\n }\n\n /** Applies waiting points (and the end, when asked) if the world is between batches. */\n flush(): RasterRect[] {\n const rects: RasterRect[] = [];\n if (this.init.world.busy || this.finished) return rects;\n while (this.queue.length) {\n const raw = this.queue.shift()!;\n this.steps.push(this.init.world.clock);\n this.lastRaw = raw;\n const p = this.smoother.push(raw);\n this.lastFed = p;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n this.states[i].walker.push(c.map(p), dabs);\n this.applyDabs(i, dabs, rects);\n });\n }\n if (this.finishing) {\n const raw = this.lastRaw;\n const fed = this.lastFed;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);\n this.states[i].walker.finish(dabs);\n this.applyDabs(i, dabs, rects);\n });\n this.finished = true;\n }\n for (const r of rects) this.bounds = union(this.bounds, r);\n return rects;\n }\n\n private applyDabs(copy: number, dabs: PenDab[], rects: RasterRect[]): void {\n for (const d of dabs) {\n const rect = dabRect(d, this.init.brush.tip.hardness);\n if (rect.width <= 0 || rect.height <= 0) continue;\n this.dab(copy, d, rect);\n rects.push(rect);\n this.dabCount++;\n }\n }\n\n /** The tip's coverage over `rect`, with the grain, quantised. */\n private coverage(d: PenDab, rect: RasterRect): { tip: Float32Array; cov: Float32Array } {\n const n = rect.width * rect.height;\n if (this.tip.length < n) {\n this.tip = new Float32Array(n);\n this.cov = new Float32Array(n);\n }\n const tip = this.tip.subarray(0, n);\n const cov = this.cov.subarray(0, n);\n tip.fill(0);\n rasterizeDab({ ...d, alpha: 1 }, this.init.brush.tip.hardness, rect, tip);\n const grain = this.depth > 0 ? this.init.grain : null;\n const mask = this.init.mask;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, k++) {\n if (mask && tip[k] > 0) {\n const mx = rect.x + xx;\n const my = rect.y + yy;\n tip[k] *= mx >= 0 && my >= 0 && mx < mask.width && my < mask.height ? mask.data[my * mask.width + mx] / 255 : 0;\n }\n let v = tip[k];\n if (v > 0 && grain) v *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, d.pressure);\n tip[k] = Math.round(tip[k] * COVERAGE_STEPS) / COVERAGE_STEPS;\n cov[k] = Math.round(v * COVERAGE_STEPS) / COVERAGE_STEPS;\n }\n }\n return { tip, cov };\n }\n\n private dab(copy: number, d: PenDab, rect: RasterRect): void {\n const { world, laneId, brush } = this.init;\n const wet = brush.wet;\n const st = this.states[copy];\n const { tip, cov } = this.coverage(d, rect);\n // How hard this dab works: flow, pressure (pressureOpacity) and the tip's opacity.\n const k = Math.round(Math.min(1, d.alpha * d.opacity * brush.tip.opacity) * COVERAGE_STEPS) / COVERAGE_STEPS;\n if (!(k > 0)) return;\n switch (wet.tool) {\n case 'paint': {\n const ex = st.exchange!;\n let flow = 1;\n if (this.sampler) {\n // Gradient paint: this dab's pigment.\n const g = this.sampler.dab(d, this.sample);\n ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);\n flow = g.alpha;\n }\n ex.pickUp(this.medium.sample(rect, tip));\n const total = ex.paint(this.paint);\n if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);\n else world.paint(laneId, rect, cov, this.medium.options.water * k, null);\n ex.drain();\n if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);\n break;\n }\n case 'water': {\n world.paint(laneId, rect, cov, this.medium.options.water * k * (wet.water > 0 ? 1 : 0), null);\n if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);\n break;\n }\n case 'dry':\n world.remove(laneId, rect, cov, wet.strength * 0.35 * k, 0, 0, wet.strength * 0.35 * k);\n break;\n case 'blow': {\n // Along the direction of travel (dab directions are counter-clockwise with y up).\n const s = wet.strength * 0.08 * k;\n world.push(laneId, rect, cov, Math.cos(d.direction) * s, -Math.sin(d.direction) * s);\n break;\n }\n case 'blend':\n world.blend(laneId, rect, cov, wet.strength * k);\n break;\n case 'smudge': {\n if (st.last) world.drag(laneId, rect, cov, d.x - st.last.x, d.y - st.last.y, wet.strength * k);\n break;\n }\n case 'blot':\n world.remove(laneId, rect, cov, wet.strength * 0.8 * k, wet.strength * 0.6 * k, wet.strength * 0.15 * k, wet.strength * 0.5 * k);\n break;\n }\n st.last = d;\n }\n}\n\n/** The wet settings each medium gives a layer the first time it holds wet paint. */\nexport const WET_MEDIUM_PARAMS: Readonly<Record<WetMediumKind, Partial<WetParams>>> = {\n watercolor: {},\n gouache: { viscosity: 0.45, edgeDarkening: 0.15, granulation: 0.1, bloom: 0.1, bleed: 0.15, pigmentWeight: 0.5 },\n oil: { viscosity: 0.95, evaporation: 0.03, edgeDarkening: 0, granulation: 0, bloom: 0, bleed: 0.08, pickup: 0.6, fixture: 0.02, pigmentWeight: 0.8 },\n};\n\n/** Settings of one wet layer (saved with the document). */\nexport interface WetLayerSettings {\n params: WetParams;\n flow: WetFlowSpec;\n}\n\n/** One wet layer as tools describe it. */\nexport interface WetLayerInfo {\n layerId: string;\n /** Surface water anywhere on the layer. */\n wet: boolean;\n /** Cells with surface water. */\n wetCells: number;\n /** Where the surface water is, or `null`. */\n wetBounds: RasterRect | null;\n /** 256 px tiles being simulated. */\n activeTiles: number;\n params: WetParams;\n flow: WetFlowSpec;\n}\n\n/** An event recorded with the clock it happened at (strokes are recorded separately), for replays. */\nexport interface WetEvent {\n clock: number;\n /** How many strokes were recorded before it (where it falls between strokes in a replay). */\n at?: number;\n kind: 'dry' | 'tilt' | 'params' | 'flow' | 'wet-paper';\n layerId?: string;\n data?: unknown;\n}\n\nexport interface PaintWetHost {\n canvas(): { width: number; height: number };\n paper(): PaperSettings;\n /** The layer's current pixels (`null` when it is not a pixel layer). */\n layerSurface(id: string): TiledSurface | null;\n /** Shows new pixels for wet layers (not an undo step); `rects` are where they changed. */\n updateSurfaces(surfaces: ReadonlyMap<string, TiledSurface>, rects: RasterRect[]): void;\n /** The paint dried while time passed (the editor records nothing, it just redraws). */\n changed(): void;\n}\n\nexport interface PaintWetOptions extends WetWorldOptions {\n /** Steps per second while time runs (default 120). */\n speed?: number;\n /** Bytes of wet snapshots kept for undo (default 512 MB). */\n undoMemory?: number;\n /**\n * Engine B, the WebGPU accelerator for large wet areas: `auto` (default: used when WebGPU is\n * there and at least `gpuMinTiles` tiles are wet) or `off` (the CPU does everything).\n */\n gpu?: WetGpuMode;\n /** Live 256 px tiles at which the GPU takes over (default 16, a fully wet 1024² canvas). */\n gpuMinTiles?: number;\n /** The WebGPU to use (default `navigator.gpu`; tests pass Node's). */\n gpuProvider?: WetGpuProvider | null;\n}\n\n/** Whether Engine B (WebGPU) may run: `auto`, or `off` to force the CPU. */\nexport type WetGpuMode = 'auto' | 'off';\nexport const WET_GPU_MODES: readonly WetGpuMode[] = ['auto', 'off'];\n\n/**\n * Engine B as panels and tools show it. `state`: `off` (forced to the CPU), `not-started` (it\n * starts with the first wet stroke), `starting`, `unavailable` (no usable WebGPU: see `reason`),\n * `standby` (ready; the wet area is below `minTiles`), `active` (the GPU shades the wet paint), or\n * `failed` (it stopped: see `reason`). The CPU stays the authority in every state.\n */\nexport interface WetEngineInfo {\n mode: WetGpuMode;\n state: 'off' | 'not-started' | 'starting' | 'unavailable' | 'standby' | 'active' | 'failed';\n reason: string | null;\n /** The GPU, when known. */\n adapter: string | null;\n /** Most wet tiles the GPU can hold. */\n maxTiles: number;\n /** Live tiles at which the GPU takes over. */\n minTiles: number;\n /** Live tiles now (all wet layers). */\n activeTiles: number;\n /** The CPU kernel (the authority), or `null` before the first wet stroke. */\n cpu: WetBackendInfo | null;\n}\n\n/** Steps the GPU may show ahead of the CPU between strokes (the longest CPU batch then). */\nexport const GPU_LEAD = 16;\n\n/** Tiles reshaded per frame at most (the rest catch up over the next frames). */\nconst DISPLAY_TILES_PER_TICK = 24;\n\n/** The paint editor's wet layers: one `WetWorld` per canvas, its settings and time. */\nexport class PaintWetLayers {\n world: WetWorld | null = null;\n tilt: WetTilt = { ...FLAT };\n readonly settings = new Map<string, WetLayerSettings>();\n paused = false;\n /** Wet strokes dry as soon as they end (one undo step with the stroke). */\n instantDry = false;\n speed: number;\n overlay = false;\n readonly undoMemory: number;\n /** The live wet stroke, if any. */\n driver: WetStrokeDriver | null = null;\n private lastTick: number | null = null;\n private ticking: Promise<void> | null = null;\n private stepsOwed = 0;\n private displaying: Promise<void> | null = null;\n private leadAt = 0;\n gpuMode: WetGpuMode;\n gpuMinTiles: number;\n private gpu: WetGpuEngine | null = null;\n private gpuSupport: WetGpuSupport | null = null;\n private gpuStarting: Promise<void> | null = null;\n\n constructor(\n private readonly host: PaintWetHost,\n private readonly options: PaintWetOptions = {}\n ) {\n this.speed = Math.max(1, Math.min(2000, options.speed ?? 120));\n this.undoMemory = Math.max(0, options.undoMemory ?? 512 * 1024 * 1024);\n this.gpuMode = options.gpu === 'off' ? 'off' : 'auto';\n this.gpuMinTiles = Math.max(1, Math.floor(options.gpuMinTiles ?? 16));\n }\n\n // --- Engine B ------------------------------------------------------------------\n\n /** Starts the GPU engine for the world (once; it may turn out unavailable). */\n private startGpu(): void {\n if (this.gpuMode === 'off' || this.gpu || this.gpuStarting || this.gpuSupport) return;\n const provider = this.options.gpuProvider === undefined ? findWebGpu() : this.options.gpuProvider;\n this.gpuStarting = createWetGpuEngine(provider)\n .then(({ engine, support }) => {\n this.gpuSupport = support;\n if (!engine) return;\n if (this.gpuMode === 'off' || !this.world) {\n engine.destroy();\n this.gpuSupport = null;\n return;\n }\n this.gpu = engine;\n this.world.gpuMinTiles = this.gpuMinTiles;\n this.world.attachGpu(engine);\n })\n .finally(() => {\n this.gpuStarting = null;\n this.host.changed();\n });\n }\n\n /** Resolves once the GPU engine has started or turned out unavailable. */\n async gpuReady(): Promise<WetEngineInfo> {\n if (this.gpuStarting) await this.gpuStarting;\n return this.engineInfo();\n }\n\n /** `auto`: use the GPU for large wet areas when there is one; `off`: the CPU only. */\n setGpu(mode: WetGpuMode, minTiles?: number): WetEngineInfo {\n if (minTiles !== undefined) {\n this.gpuMinTiles = Math.max(1, Math.floor(minTiles));\n if (this.world) this.world.gpuMinTiles = this.gpuMinTiles;\n }\n if (mode === this.gpuMode) return this.engineInfo();\n this.gpuMode = mode;\n if (mode === 'off') {\n this.world?.attachGpu(null);\n this.gpu?.destroy();\n this.gpu = null;\n this.gpuSupport = null;\n } else if (this.world) this.startGpu();\n return this.engineInfo();\n }\n\n engineInfo(): WetEngineInfo {\n const w = this.world;\n const base = { mode: this.gpuMode, adapter: this.gpuSupport?.adapter ?? null, maxTiles: this.gpuSupport?.maxSlots ?? 0, minTiles: this.gpuMinTiles, activeTiles: w?.activeTiles() ?? 0, cpu: this.backend() };\n if (this.gpuMode === 'off') return { ...base, state: 'off', reason: 'Forced to the CPU.' };\n if (this.gpuStarting) return { ...base, state: 'starting', reason: null };\n if (w?.gpuError) return { ...base, state: 'failed', reason: w.gpuError };\n if (this.gpuSupport && !this.gpuSupport.supported) return { ...base, state: 'unavailable', reason: this.gpuSupport.reason };\n if (!this.gpu) return { ...base, state: 'not-started', reason: 'Starts with the first wet stroke.' };\n if (w?.gpuActive) return { ...base, state: 'active', reason: null };\n return { ...base, state: 'standby', reason: `The GPU takes over at ${this.gpuMinTiles} wet tiles (256 px).` };\n }\n\n /** The world (made on first use, at the canvas size). */\n ensureWorld(): WetWorld {\n const { width, height } = this.host.canvas();\n if (this.world && (this.world.width !== width || this.world.height !== height)) throw new Error('The wet world is the wrong size; dry the paint before resizing the canvas.');\n if (!this.world) {\n this.world = new WetWorld(width, height, wetPaperInput(this.host.paper()), { kernel: this.options.kernel, threads: this.options.threads });\n this.world.setTilt(this.tilt);\n this.world.gpuMinTiles = this.gpuMinTiles;\n if (this.gpu) this.world.attachGpu(this.gpu);\n else this.startGpu();\n }\n return this.world;\n }\n\n /** Whether any layer (or `layerId`) has wet paint. */\n isWet(layerId?: string): boolean {\n const w = this.world;\n if (!w) return false;\n if (layerId !== undefined) return w.hasLane(layerId) && w.isActive(layerId);\n return w.laneIds().some((id) => w.isActive(id));\n }\n\n /** Layers holding a wet layer (wet or drying). */\n wetLayerIds(): string[] {\n return this.world?.laneIds() ?? [];\n }\n\n /** Whether a batch is running (edits wait). */\n get busy(): boolean {\n return Boolean(this.world?.busy);\n }\n\n /** Resolves when no batch is running. */\n async idle(): Promise<void> {\n while (this.ticking) await this.ticking;\n }\n\n settingsOf(layerId: string, medium: WetMediumKind = 'watercolor'): WetLayerSettings {\n let s = this.settings.get(layerId);\n if (!s) {\n s = { params: normalizeWetParams(WET_MEDIUM_PARAMS[medium], DEFAULT_WET_PARAMS), flow: { ...DEFAULT_FLOW } };\n this.settings.set(layerId, s);\n }\n return s;\n }\n\n /** Puts a layer in the world (its current pixels become the base under the wet paint). */\n ensureLane(layerId: string, medium?: WetMediumKind): WetWorld {\n const w = this.ensureWorld();\n if (!w.hasLane(layerId)) {\n const base = this.host.layerSurface(layerId) ?? new TiledSurface(4, new Map(), 0);\n const s = this.settingsOf(layerId, medium);\n w.ensureLane(layerId, { params: s.params, base });\n const { width, height } = this.host.canvas();\n if (s.flow.kind !== 'none') w.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);\n }\n return w;\n }\n\n setParams(layerId: string, update: Partial<WetParams>): WetParams {\n const s = this.settingsOf(layerId);\n s.params = normalizeWetParams(update, s.params);\n if (this.world?.hasLane(layerId)) this.world.setParams(layerId, s.params);\n return { ...s.params };\n }\n\n setFlow(layerId: string, update: Partial<WetFlowSpec>): WetFlowSpec {\n const s = this.settingsOf(layerId);\n s.flow = normalizeFlow(update, s.flow);\n if (this.world?.hasLane(layerId)) {\n const { width, height } = this.host.canvas();\n this.world.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);\n }\n return { ...s.flow };\n }\n\n setTilt(update: Partial<WetTilt>): WetTilt {\n this.tilt = normalizeTilt(update, this.tilt);\n this.world?.setTilt(this.tilt);\n return { ...this.tilt };\n }\n\n /** The paper changed. */\n paperChanged(): void {\n this.world?.setPaper(wetPaperInput(this.host.paper()));\n }\n\n /** A layer as tools describe it. */\n info(layerId: string): WetLayerInfo {\n const s = this.settingsOf(layerId);\n const st: WetLaneStats | null = this.world?.hasLane(layerId) ? this.world.stats(layerId) : null;\n return {\n layerId,\n wet: Boolean(st && st.wetCells > 0),\n wetCells: st?.wetCells ?? 0,\n wetBounds: st?.wetBounds ?? null,\n activeTiles: st?.activeTiles ?? 0,\n params: { ...s.params },\n flow: { ...s.flow },\n };\n }\n\n backend(): WetBackendInfo | null {\n return this.world?.backend() ?? null;\n }\n\n /** Shows the current wet pixels (at most `maxTiles` reshaded per layer). */\n syncDisplays(maxTiles = Infinity): RasterRect[] {\n const w = this.world;\n if (!w) return [];\n const surfaces = new Map<string, TiledSurface>();\n const rects: RasterRect[] = [];\n for (const id of w.laneIds()) {\n const d = w.display(id, maxTiles);\n if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);\n rects.push(...d.changed);\n }\n if (surfaces.size) this.host.updateSurfaces(surfaces, rects);\n return rects;\n }\n\n /** Like `syncDisplays`, shading on the GPU when Engine B is active (all stale tiles at once). */\n async syncDisplaysGpu(maxCpuTiles: number, lead = false): Promise<RasterRect[]> {\n const w = this.world;\n if (!w) return [];\n const surfaces = new Map<string, TiledSurface>();\n const rects: RasterRect[] = [];\n for (const id of w.laneIds()) {\n const d = await w.displayGpu(id, maxCpuTiles, lead);\n if (!w.hasLane(id)) continue;\n if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);\n rects.push(...d.changed);\n }\n if (surfaces.size) this.host.updateSurfaces(surfaces, rects);\n return rects;\n }\n\n /** Drops the wet layers whose paint has all dried (they become ordinary pixel layers). */\n private retireDry(): void {\n const w = this.world;\n if (!w) return;\n for (const id of w.laneIds()) {\n if (w.isActive(id) || this.driver?.init.laneId === id) continue;\n const base = w.getBase(id);\n w.removeLane(id);\n this.host.updateSurfaces(new Map([[id, base]]), []);\n }\n }\n\n /**\n * Advances time by the wall clock (`now` in ms, e.g. `performance.now()`): `speed` steps per\n * second, at most `maxSteps` per call. Call it every frame; it does nothing while paused or dry.\n * Resolves when the steps are done and the display is up to date.\n */\n tick(now: number, maxSteps = 8): Promise<void> {\n if (this.ticking) {\n // Between strokes, while the CPU works through a batch, the GPU shows its steps as they come.\n this.leadFrame(now);\n return this.ticking;\n }\n // The GPU is still shading the last frame: wait for it (edits need not; they just win).\n if (this.displaying) return this.displaying;\n const last = this.lastTick;\n this.lastTick = now;\n const w = this.world;\n if (!w || this.paused || last === null) {\n this.flushDriver();\n return Promise.resolve();\n }\n if (!w.isActive()) {\n this.flushDriver();\n this.retireDry();\n return Promise.resolve();\n }\n // Engine B between strokes: longer CPU batches (up to GPU_LEAD steps) that the GPU runs ahead through.\n const lead = w.gpuActive && !this.driver;\n w.gpuLeadMode = lead;\n const most = lead ? Math.max(maxSteps, GPU_LEAD) : maxSteps;\n this.stepsOwed = Math.min(most * 4, this.stepsOwed + ((now - last) / 1000) * this.speed);\n const n = Math.min(most, Math.floor(this.stepsOwed));\n this.leadAt = now;\n if (n < 1) {\n this.flushDriver();\n return Promise.resolve();\n }\n this.stepsOwed -= n;\n const run = (async () => {\n try {\n await w.step(n);\n } finally {\n this.ticking = null;\n }\n this.flushDriver();\n if (w.gpuActive) {\n const shading = this.syncDisplaysGpu(DISPLAY_TILES_PER_TICK).then(() => undefined);\n this.displaying = shading;\n try {\n await shading;\n } finally {\n this.displaying = null;\n }\n } else this.syncDisplays(DISPLAY_TILES_PER_TICK);\n this.retireDry();\n this.host.changed();\n })();\n this.ticking = run;\n return run;\n }\n\n /** Lead mode: steps the GPU on by the time since the last frame (within the running batch) and shows it. */\n private leadFrame(now: number): void {\n const w = this.world;\n if (!w || this.displaying || this.driver || !w.gpuLeading) return;\n const k = Math.max(1, Math.round(((now - this.leadAt) / 1000) * this.speed));\n if (!w.gpuLead(k)) return;\n this.leadAt = now;\n const shading = this.syncDisplaysGpu(0, true).then(() => this.host.changed());\n this.displaying = shading;\n void shading.finally(() => {\n if (this.displaying === shading) this.displaying = null;\n });\n }\n\n private flushDriver(): void {\n const d = this.driver;\n if (!d || !this.world) return;\n const rects = d.flush();\n if (rects.length) this.syncDisplays(DISPLAY_TILES_PER_TICK);\n if (d.done) this.onDriverDone?.(d);\n }\n\n /** Called when a live stroke that ended mid-batch has finished applying. */\n onDriverDone: ((driver: WetStrokeDriver) => void) | null = null;\n\n /** Steps `steps` times now (agents, tests). Returns the steps run. */\n async advance(steps: number): Promise<number> {\n await this.idle();\n const w = this.world;\n if (!w) return 0;\n // The fastest kernel (it gives the same bits; the TypeScript one is only a stand-in while it loads).\n await w.ready;\n await this.idle();\n const start = w.clock;\n const n = Math.max(0, Math.floor(steps));\n const job = (async () => {\n try {\n await w.step(n);\n } finally {\n this.ticking = null;\n }\n })();\n this.ticking = job;\n await job;\n if (w.gpuActive) await this.syncDisplaysGpu(Infinity);\n else this.syncDisplays();\n this.retireDry();\n return w.clock - start;\n }\n\n /**\n * Dries at once (everything settles where it is), on one layer or all, optionally only in a\n * rectangle. Returns the layers and rectangles that dried.\n */\n dry(layerId?: string, rect?: RasterRect): Array<{ layerId: string; bounds: RasterRect | null }> {\n const w = this.world;\n if (!w) return [];\n const ids = layerId !== undefined ? (w.hasLane(layerId) ? [layerId] : []) : w.laneIds();\n const out: Array<{ layerId: string; bounds: RasterRect | null }> = [];\n for (const id of ids) out.push({ layerId: id, bounds: w.dry(id, rect) });\n this.syncDisplays();\n this.retireDry();\n return out;\n }\n\n /** The layer's pixels with all its wet paint dried (deterministic), without changing anything. */\n bakedSurface(layerId: string): TiledSurface | null {\n const w = this.world;\n if (!w?.hasLane(layerId)) return null;\n return w.bakedSurface(layerId);\n }\n\n /** A snapshot of the world for undo (`null` when there is none). */\n snapshot(): WetSnapshot | null {\n const w = this.world;\n if (!w || !w.laneIds().length) return null;\n return w.snapshot();\n }\n\n /** Puts a snapshot back (`null`: no wet layers). Returns the layer surfaces it implies. */\n restore(snap: WetSnapshot | null): void {\n if (!snap) {\n if (this.world) for (const id of this.world.laneIds()) this.world.removeLane(id);\n return;\n }\n this.ensureWorld().restore(snap);\n this.syncDisplays();\n }\n\n /** Forgets everything wet (a new canvas, a resize, opening a document). */\n reset(): void {\n this.world?.destroy();\n this.world = null;\n this.driver = null;\n this.settings.clear();\n this.lastTick = null;\n this.stepsOwed = 0;\n }\n\n /** Water overlay tiles (RGBA) over a document rectangle, for the view. */\n waterOverlay(rect: RasterRect): Array<{ tx: number; ty: number; data: Uint8ClampedArray }> {\n const w = this.world;\n if (!w) return [];\n const out: Array<{ tx: number; ty: number; data: Uint8ClampedArray }> = [];\n const tx0 = Math.max(0, Math.floor(rect.x / TILE));\n const ty0 = Math.max(0, Math.floor(rect.y / TILE));\n const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);\n const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);\n for (let ty = ty0; ty <= ty1; ty++)\n for (let tx = tx0; tx <= tx1; tx++) {\n let merged: Uint8ClampedArray | null = null;\n for (const id of w.laneIds()) {\n const t = w.waterOverlay(id, tx, ty);\n if (!t) continue;\n if (!merged) merged = t;\n else for (let i = 3; i < t.length; i += 4) merged[i] = Math.max(merged[i], t[i]);\n }\n if (merged) out.push({ tx, ty, data: merged });\n }\n return out;\n }\n\n destroy(): void {\n this.world?.destroy();\n this.world = null;\n this.gpu?.destroy();\n this.gpu = null;\n }\n}\n\n/** The pigment (K r, g, b, S) of a colour at a body, per unit mass (for tools that report it). */\nexport function wetPigmentOf(hex: string, body: number): number[] {\n const [r, g, b] = hexToRgb8(hex);\n return Array.from(pigmentOf([r / 255, g / 255, b / 255], body));\n}\n","/**\n * Brush thumbnails: a test stroke (an S-curve with pressure swelling and fading, at a steady\n * speed) painted with the brush on the paper. Brushes that only move paint (blender, smudge) are\n * shown dragged across two bands of colour. Pure TypeScript, so thumbnails render in Node too.\n */\n\nimport { EMPTY_MIXER_LOAD } from '../paint/mixer';\nimport { emptySurface, fillRect, type TiledSurface } from '../raster/surface';\nimport { normalizePaintBrush, type PaintBrush } from './brushes';\nimport type { PenPoint } from './engine';\nimport { IDENTITY_COPY } from './engine';\nimport { hexToRgb8, type PaperSettings } from './paper';\nimport { createPaintCompositor, paperGrain, paperNode, type ViewLayer } from './render';\nimport { brushLoadForStroke, PaintStrokeSession } from './stroke';\nimport { WetWorld } from '../paint/wet/world';\nimport { pigmentOf } from '../paint/wet/film';\nimport { WetStrokeDriver, wetPaperInput } from './wet';\nimport { DEFAULT_LIGHTING, emptyThickChannels, LitCache, type ThickChannels } from './thick';\n\n/** One small wet world reused for wet brush thumbnails (by size). */\nlet thumbWorld: WetWorld | null = null;\n\n/** A wet brush's test stroke: laid on a wet layer (over a wet band of colour for the tools that move paint), shown as painted, before any time passes. */\nfunction paintWetTestStroke(brush: PaintBrush, paper: PaperSettings, color: string, width: number, height: number): TiledSurface {\n if (!thumbWorld || thumbWorld.width !== width || thumbWorld.height !== height) {\n thumbWorld?.destroy();\n thumbWorld = new WetWorld(width, height, wetPaperInput(paper), { kernel: 'typescript', threads: 0 });\n } else thumbWorld.setPaper(wetPaperInput(paper));\n const w = thumbWorld;\n if (w.hasLane('thumb')) w.removeLane('thumb');\n w.ensureLane('thumb');\n const [r, g, b] = hexToRgb8(color);\n if (brush.wet.tool !== 'paint' && brush.wet.tool !== 'water') {\n // Wet paint to work in: the colour over ochre, as for the dry brushes that move paint.\n const half = Math.ceil(height / 2);\n w.paint('thumb', { x: 0, y: 0, width, height: half }, null, 0.4, Array.from(pigmentOf([r / 255, g / 255, b / 255], 0), (v) => v * 0.05));\n w.paint('thumb', { x: 0, y: half, width, height: height - half }, null, 0.4, Array.from(pigmentOf([232 / 255, 196 / 255, 64 / 255], 0), (v) => v * 0.05));\n }\n const driver = new WetStrokeDriver({\n world: w,\n laneId: 'thumb',\n brush,\n color: [r / 255, g / 255, b / 255],\n load: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),\n grain: paperGrain(paper),\n paperDepth: paper.depth,\n paperColor: hexToRgb8(paper.color),\n seed: 7,\n copies: [IDENTITY_COPY],\n });\n driver.push(thumbnailStroke(width, height));\n driver.finish();\n // Blowing only shows once the water has moved.\n if (brush.wet.tool === 'blow') w.stepSync(4);\n return w.display('thumb').surface;\n}\n\nexport interface BrushThumbnail {\n width: number;\n height: number;\n /** RGBA, opaque (the paper is under the stroke). */\n data: Uint8ClampedArray;\n}\n\n/** The test stroke's samples for a `width × height` thumbnail. */\nexport function thumbnailStroke(width: number, height: number): PenPoint[] {\n const pts: PenPoint[] = [];\n const n = 64;\n const mx = width * 0.1;\n const my = height * 0.5;\n for (let i = 0; i <= n; i++) {\n const t = i / n;\n const pressure = t < 0.5 ? 0.15 + 1.7 * t : 1 - 1.4 * (t - 0.5);\n pts.push({ x: mx + (width - 2 * mx) * t, y: my - Math.sin(t * Math.PI * 2) * height * 0.22, pressure, t: i * 12 });\n }\n return pts;\n}\n\n/** Paints the test stroke with `brush` onto `surface`; returns the new pixels. */\nfunction paintTestStroke(surface: TiledSurface, brush: PaintBrush, paper: PaperSettings, color: string, width: number, height: number): TiledSurface {\n const [r, g, b] = hexToRgb8(color);\n let result = surface;\n let thick: ThickChannels | null = null;\n const session = new PaintStrokeSession(\n {\n update: () => {},\n commit: (sess, s) => {\n result = s ?? result;\n thick = sess.thickChannels();\n },\n },\n {\n layerId: 'thumbnail',\n surface,\n offset: { x: 0, y: 0 },\n lockTransparency: false,\n brush,\n color: [r, g, b, 255],\n background: [...hexToRgb8(paper.color), 255],\n grain: paperGrain(paper),\n paperDepth: paper.depth,\n seed: 7,\n colorHex: color,\n affineCopies: [],\n copies: [IDENTITY_COPY],\n mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),\n thick: brush.engine === 'impasto' ? { channels: emptyThickChannels() } : undefined,\n sticker: brush.engine === 'sticker' ? { nextId: 1 } : undefined,\n }\n );\n session.push(thumbnailStroke(width, height));\n session.end();\n // Thick paint is shown lit, as on the canvas.\n return thick ? new LitCache().lit(result, thick, DEFAULT_LIGHTING) : result;\n}\n\nexport function renderBrushThumbnail(brush: PaintBrush, options: { paper: PaperSettings; width?: number; height?: number; color: string }): BrushThumbnail {\n const width = Math.max(16, Math.min(512, Math.round(options.width ?? 160)));\n const height = Math.max(16, Math.min(512, Math.round(options.height ?? 48)));\n // Fit the tip in the thumbnail: big brushes are drawn smaller.\n const maxSize = height * 0.42;\n const b = brush.tip.size > maxSize ? normalizePaintBrush({ tip: { ...brush.tip, size: maxSize } }, brush) : brush;\n const rgba = [...hexToRgb8(options.color), 255];\n let surface: TiledSurface = emptySurface(4);\n if (b.engine === 'eraser') {\n // Something to erase: a band of the colour.\n surface = fillRect(surface, { x: 0, y: 0, width, height }, rgba);\n } else if (b.engine === 'wet') {\n // the wet layer draws its own band\n } else if (b.engine === 'blender' || b.engine === 'smudge' || (b.engine === 'mixer' && !b.loadsColor) || b.behavior.pickup > 0) {\n // Something to move: two bands, the colour over ochre.\n const half = Math.ceil(height / 2);\n surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);\n surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);\n }\n surface = b.engine === 'wet' ? paintWetTestStroke(b, options.paper, options.color, width, height) : paintTestStroke(surface, b, options.paper, options.color, width, height);\n const layer: ViewLayer = { type: 'pixel', id: 'thumbnail', name: 'thumbnail', visible: true, opacity: 1, blend: 'normal', surface, offset: { x: 0, y: 0 } };\n const data = createPaintCompositor().composite([paperNode(options.paper), layer], { x: 0, y: 0, width, height }, { width, height });\n return { width, height, data };\n}\n","// Portions ported from photocraft/crates/paint/src/symmetry.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Painting guides: mirror and kaleidoscope symmetry (every stroke paints its reflected or rotated\n * copies as one stroke), perspective guides with one to three vanishing points that strokes snap\n * to, a tracing image under the paint (optionally the source of the stroke colour), and floating\n * reference images to pick colours from. Geometry only: no pixels, no DOM.\n */\n\nimport { composeAffine, type Affine } from './strokes';\nimport type { Stencil } from './stencils';\n\nexport type SymmetryMode = 'off' | 'vertical' | 'horizontal' | 'dual' | 'diagonal' | 'kaleidoscope';\n\nexport const SYMMETRY_MODES: readonly SymmetryMode[] = ['off', 'vertical', 'horizontal', 'dual', 'diagonal', 'kaleidoscope'];\n\nexport interface SymmetryGuide {\n mode: SymmetryMode;\n /** Centre of the axes in document pixels (default the canvas centre). */\n center: { x: number; y: number };\n /** Turns the axes, degrees clockwise (default 0). */\n rotation: number;\n /** Kaleidoscope: number of segments, 2..32 (default 6). */\n segments: number;\n /** Kaleidoscope: reflect every other segment as well as rotating (default true), like a real kaleidoscope. */\n mirror: boolean;\n}\n\nexport interface Point2 {\n x: number;\n y: number;\n}\n\nexport interface PerspectiveGuide {\n enabled: boolean;\n /** One to three vanishing points in document pixels (they may lie outside the canvas). */\n points: Point2[];\n /** Strokes snap to the guide lines (default true when enabled). */\n snap: boolean;\n}\n\n/** A view-only image under the paint, placed over the canvas. */\nexport interface TracingImage {\n /** PNG or JPEG data URL. */\n src: string;\n /** Natural size of the image in pixels. */\n width: number;\n height: number;\n /** Where it is drawn, in document pixels (default: fitted to the canvas). */\n rect: { x: number; y: number; width: number; height: number };\n /** 0..1 (default 0.35). */\n opacity: number;\n visible: boolean;\n /** Strokes without an explicit colour take the tracing image's colour where they start. */\n sampleColors: boolean;\n}\n\n/** A floating image to look at and pick colours from (shown beside the canvas, never painted). */\nexport interface ReferenceImage {\n id: string;\n name: string;\n src: string;\n width: number;\n height: number;\n /** Where the panel floats over the viewport, in screen pixels, and its displayed width. */\n x: number;\n y: number;\n displayWidth: number;\n visible: boolean;\n}\n\nexport interface PaintGuides {\n symmetry: SymmetryGuide;\n perspective: PerspectiveGuide;\n tracing: TracingImage | null;\n references: ReferenceImage[];\n /** Rulers, arcs, straight edges and shape templates that guide or mask strokes. */\n stencils: Stencil[];\n}\n\nexport function defaultGuides(canvas: { width: number; height: number }): PaintGuides {\n return {\n symmetry: { mode: 'off', center: { x: canvas.width / 2, y: canvas.height / 2 }, rotation: 0, segments: 6, mirror: true },\n perspective: { enabled: false, points: [], snap: true },\n tracing: null,\n references: [],\n stencils: [],\n };\n}\n\nexport const MAX_COORDINATE = 1_000_000;\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst coord = (v: unknown, d: number) => (finite(v) && Math.abs(v) <= MAX_COORDINATE ? v : d);\n\nexport function normalizeSymmetry(raw: Partial<SymmetryGuide> | Record<string, unknown>, base: SymmetryGuide): SymmetryGuide {\n const r = raw as Record<string, unknown>;\n const c = (r.center ?? {}) as Record<string, unknown>;\n return {\n mode: SYMMETRY_MODES.includes(r.mode as SymmetryMode) ? (r.mode as SymmetryMode) : base.mode,\n center: { x: coord(c.x, base.center.x), y: coord(c.y, base.center.y) },\n rotation: finite(r.rotation) ? ((r.rotation % 360) + 360) % 360 : base.rotation,\n segments: finite(r.segments) ? Math.min(32, Math.max(2, Math.round(r.segments))) : base.segments,\n mirror: typeof r.mirror === 'boolean' ? r.mirror : base.mirror,\n };\n}\n\n/** Reflection across the line through `c` in direction angle `a` (radians, clockwise with y down). */\nfunction reflectAbout(c: Point2, a: number): Affine {\n const dx = Math.cos(a);\n const dy = Math.sin(a);\n // 2·d·dᵀ − I, conjugated by the translation to the centre.\n const m00 = 2 * dx * dx - 1;\n const m01 = 2 * dx * dy;\n const m11 = 2 * dy * dy - 1;\n return [m00, m01, m01, m11, c.x - (m00 * c.x + m01 * c.y), c.y - (m01 * c.x + m11 * c.y)];\n}\n\n/** Rotation by `a` radians (clockwise with y down) about `c`. */\nfunction rotateAbout(c: Point2, a: number): Affine {\n const cos = Math.cos(a);\n const sin = Math.sin(a);\n return [cos, -sin, sin, cos, c.x - (cos * c.x - sin * c.y), c.y - (sin * c.x + cos * c.y)];\n}\n\nconst rad = (deg: number) => (deg * Math.PI) / 180;\n\n/** The axis directions of a mirror mode (radians), as photocraft's `directions`. */\nexport function symmetryAxisAngles(g: SymmetryGuide): number[] {\n if (g.mode === 'off') return [];\n if (g.mode === 'kaleidoscope') return Array.from({ length: g.segments }, (_, k) => rad(g.rotation) + (k * Math.PI) / g.segments);\n const initial = g.mode === 'horizontal' ? 0 : g.mode === 'diagonal' ? 45 : 90;\n const a = rad(initial + g.rotation);\n return g.mode === 'dual' ? [a, a + Math.PI / 2] : [a];\n}\n\n/**\n * The extra copies a stroke paints under the symmetry guide, as affine maps (the stroke itself is\n * not included). Vertical, horizontal and diagonal: one reflection. Dual: two reflections and the\n * point reflection. Kaleidoscope: rotations by 360°/n, and with `mirror` the reflections between them.\n */\nexport function symmetryCopies(g: SymmetryGuide): Affine[] {\n const c = g.center;\n switch (g.mode) {\n case 'off':\n return [];\n case 'vertical':\n case 'horizontal':\n case 'diagonal':\n return symmetryAxisAngles(g).map((a) => reflectAbout(c, a));\n case 'dual': {\n const [a, b] = symmetryAxisAngles(g);\n return [reflectAbout(c, a), reflectAbout(c, b), rotateAbout(c, Math.PI)];\n }\n case 'kaleidoscope': {\n const out: Affine[] = [];\n const n = g.segments;\n for (let k = 1; k < n; k++) out.push(rotateAbout(c, (2 * Math.PI * k) / n));\n if (g.mirror) {\n const first = reflectAbout(c, rad(g.rotation));\n for (let k = 0; k < n; k++) out.push(composeAffine(rotateAbout(c, (2 * Math.PI * k) / n), first));\n }\n return out;\n }\n }\n}\n\n/** The axes clipped to the canvas, for drawing (photocraft's `segments`). */\nexport function symmetryAxisSegments(g: SymmetryGuide, size: { width: number; height: number }): Array<[Point2, Point2]> {\n const out: Array<[Point2, Point2]> = [];\n for (const a of symmetryAxisAngles(g)) {\n const d = [Math.cos(a), Math.sin(a)];\n const origin = [g.center.x, g.center.y];\n const extent = [size.width, size.height];\n let lo = -Infinity;\n let hi = Infinity;\n let ok = true;\n for (let i = 0; i < 2; i++) {\n if (Math.abs(d[i]) < 1e-12) {\n if (origin[i] < 0 || origin[i] > extent[i]) ok = false;\n } else {\n const t0 = -origin[i] / d[i];\n const t1 = (extent[i] - origin[i]) / d[i];\n lo = Math.max(lo, Math.min(t0, t1));\n hi = Math.min(hi, Math.max(t0, t1));\n }\n }\n if (!ok || !Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo) continue;\n out.push([{ x: g.center.x + lo * d[0], y: g.center.y + lo * d[1] }, { x: g.center.x + hi * d[0], y: g.center.y + hi * d[1] }]);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Perspective\n// ---------------------------------------------------------------------------\n\nexport function normalizePerspective(raw: Partial<PerspectiveGuide> | Record<string, unknown>, base: PerspectiveGuide): PerspectiveGuide {\n const r = raw as Record<string, unknown>;\n let points = base.points;\n if (Array.isArray(r.points)) {\n if (r.points.length > 3) throw new Error(`A perspective guide has one to three vanishing points; got ${r.points.length}.`);\n points = r.points.map((p, i) => {\n const q = (p ?? {}) as Record<string, unknown>;\n if (!finite(q.x) || !finite(q.y) || Math.abs(q.x) > MAX_COORDINATE || Math.abs(q.y) > MAX_COORDINATE) {\n throw new Error(`points[${i}] needs finite x and y within ±${MAX_COORDINATE} document pixels.`);\n }\n return { x: q.x, y: q.y };\n });\n }\n const enabled = typeof r.enabled === 'boolean' ? r.enabled : base.enabled;\n if (enabled && !points.length) throw new Error('An enabled perspective guide needs at least one vanishing point; pass points.');\n return { enabled, points, snap: typeof r.snap === 'boolean' ? r.snap : base.snap };\n}\n\n/** The line families a perspective guide snaps to at `p`: unit directions. */\nexport function perspectiveDirections(g: PerspectiveGuide, p: Point2): Point2[] {\n const out: Point2[] = [];\n for (const v of g.points) {\n const dx = v.x - p.x;\n const dy = v.y - p.y;\n const len = Math.hypot(dx, dy);\n if (len > 1e-6) out.push({ x: dx / len, y: dy / len });\n }\n // One-point perspective keeps true horizontals and verticals; two-point keeps verticals.\n if (g.points.length <= 2) out.push({ x: 0, y: 1 });\n if (g.points.length <= 1) out.push({ x: 1, y: 0 });\n return out;\n}\n\n/** How far the pen must move before a stroke chooses its perspective line (document pixels). */\nexport const SNAP_DECIDE_DISTANCE = 6;\n\n/**\n * Snaps one stroke to a perspective guide: once the pen has moved `SNAP_DECIDE_DISTANCE` from\n * where it started, the stroke picks the guide line through its start that best matches its\n * direction, and every sample after that is projected onto the line.\n */\nexport class PerspectiveSnapper {\n private start: Point2 | null = null;\n private dir: Point2 | null = null;\n\n constructor(private readonly guide: PerspectiveGuide) {}\n\n /** The line the stroke follows, once chosen. */\n get line(): { from: Point2; direction: Point2 } | null {\n return this.start && this.dir ? { from: this.start, direction: this.dir } : null;\n }\n\n snap<P extends Point2>(p: P): P {\n if (!this.guide.enabled || !this.guide.snap || !this.guide.points.length) return p;\n if (!this.start) {\n this.start = { x: p.x, y: p.y };\n return p;\n }\n const s = this.start;\n if (!this.dir) {\n const dx = p.x - s.x;\n const dy = p.y - s.y;\n const len = Math.hypot(dx, dy);\n if (len < SNAP_DECIDE_DISTANCE) return { ...p, x: s.x, y: s.y };\n let best: Point2 | null = null;\n let bestDot = -1;\n for (const d of perspectiveDirections(this.guide, s)) {\n const dot = Math.abs((dx * d.x + dy * d.y) / len);\n if (dot > bestDot) {\n bestDot = dot;\n best = d;\n }\n }\n this.dir = best;\n if (!best) return p;\n }\n const d = this.dir!;\n const t = (p.x - s.x) * d.x + (p.y - s.y) * d.y;\n return { ...p, x: s.x + d.x * t, y: s.y + d.y * t };\n }\n}\n\n/** Guide lines to draw for a perspective guide: rays from each vanishing point through the canvas. */\nexport function perspectiveGuideLines(g: PerspectiveGuide, size: { width: number; height: number }, count = 12): Array<[Point2, Point2]> {\n const out: Array<[Point2, Point2]> = [];\n const far = Math.hypot(size.width, size.height) * 4;\n for (const v of g.points) {\n // Aim the fan at the canvas: spread around the direction to its centre.\n const cx = size.width / 2 - v.x;\n const cy = size.height / 2 - v.y;\n const inside = v.x >= 0 && v.y >= 0 && v.x <= size.width && v.y <= size.height;\n const mid = Math.atan2(cy, cx);\n const spread = inside ? Math.PI : Math.min(Math.PI, 2 * Math.atan2(Math.hypot(size.width, size.height) / 2, Math.hypot(cx, cy)));\n for (let i = 0; i < count; i++) {\n const a = inside ? (2 * Math.PI * i) / count : mid - spread / 2 + (spread * (i + 0.5)) / count;\n out.push([{ ...v }, { x: v.x + Math.cos(a) * far, y: v.y + Math.sin(a) * far }]);\n }\n }\n return out;\n}\n","/**\n * Colour for painters: hex / RGB / HSV / HSL conversions, a colour wheel that is either the\n * painter's red-yellow-blue wheel (where red's complement is green) or the RGB wheel (where it is\n * cyan), harmonies on it, and swatches.\n */\n\nimport { paintSourceColor, readPaintSource, type PaintGradientSource } from '../paint/gradient-paint';\n\nexport type Rgb255 = [number, number, number];\n\nconst HEX = /^#?([0-9a-f]{6})$/i;\n\nexport function parseHexColor(value: unknown, what = 'color'): Rgb255 {\n const m = typeof value === 'string' ? HEX.exec(value.trim()) : null;\n if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. \"#2a4b8d\".`);\n const n = parseInt(m[1], 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nexport function rgbToHexColor(r: number, g: number, b: number): string {\n return '#' + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('');\n}\n\n/** RGB 0..255 → HSV with h in degrees 0..360, s and v 0..1. */\nexport function rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {\n const R = r / 255;\n const G = g / 255;\n const B = b / 255;\n const max = Math.max(R, G, B);\n const min = Math.min(R, G, B);\n const d = max - min;\n let h = 0;\n if (d > 0) {\n if (max === R) h = 60 * (((G - B) / d) % 6);\n else if (max === G) h = 60 * ((B - R) / d + 2);\n else h = 60 * ((R - G) / d + 4);\n }\n if (h < 0) h += 360;\n return { h, s: max === 0 ? 0 : d / max, v: max };\n}\n\nexport function hsvToRgb(h: number, s: number, v: number): Rgb255 {\n const hh = (((h % 360) + 360) % 360) / 60;\n const c = v * s;\n const x = c * (1 - Math.abs((hh % 2) - 1));\n const m = v - c;\n const [r, g, b] = hh < 1 ? [c, x, 0] : hh < 2 ? [x, c, 0] : hh < 3 ? [0, c, x] : hh < 4 ? [0, x, c] : hh < 5 ? [x, 0, c] : [c, 0, x];\n return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];\n}\n\n/** RGB 0..255 → HSL with h in degrees, s and l 0..1. */\nexport function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n const { h } = rgbToHsv(r, g, b);\n const max = Math.max(r, g, b) / 255;\n const min = Math.min(r, g, b) / 255;\n const l = (max + min) / 2;\n const d = max - min;\n return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };\n}\n\nexport function hslToRgb(h: number, s: number, l: number): Rgb255 {\n const c = (1 - Math.abs(2 * l - 1)) * s;\n const v = l + c / 2;\n return hsvToRgb(h, v === 0 ? 0 : c / v, v);\n}\n\n/** Which wheel hues live on: `ryb` (painter's: red ↔ green complements) or `rgb` (red ↔ cyan). */\nexport type ColorWheel = 'ryb' | 'rgb';\n\n// RYB wheel angle → RGB hue (a piecewise-linear map through red, orange, yellow, green, blue, violet).\nconst RYB_STOPS: ReadonlyArray<[number, number]> = [\n [0, 0],\n [60, 30],\n [120, 60],\n [180, 120],\n [240, 240],\n [300, 280],\n [360, 360],\n];\n\nfunction piecewise(v: number, from: 0 | 1): number {\n const x = ((v % 360) + 360) % 360;\n const to = from === 0 ? 1 : 0;\n for (let i = 1; i < RYB_STOPS.length; i++) {\n const a = RYB_STOPS[i - 1];\n const b = RYB_STOPS[i];\n if (x <= b[from]) return a[to] + ((x - a[from]) / (b[from] - a[from])) * (b[to] - a[to]);\n }\n return x;\n}\n\n/** A wheel angle (degrees) → an RGB hue. */\nexport function wheelToHue(angle: number, wheel: ColorWheel): number {\n return wheel === 'rgb' ? ((angle % 360) + 360) % 360 : piecewise(angle, 0);\n}\n\n/** An RGB hue → its wheel angle (degrees). */\nexport function hueToWheel(hue: number, wheel: ColorWheel): number {\n return wheel === 'rgb' ? ((hue % 360) + 360) % 360 : piecewise(hue, 1);\n}\n\nexport type HarmonyKind = 'complementary' | 'split-complementary' | 'analogous' | 'triadic' | 'tetradic' | 'square' | 'monochromatic';\n\nexport const HARMONY_KINDS: readonly HarmonyKind[] = ['complementary', 'split-complementary', 'analogous', 'triadic', 'tetradic', 'square', 'monochromatic'];\n\nexport interface HarmonyColor {\n color: string;\n /** Position on the wheel, degrees. */\n angle: number;\n}\n\n/**\n * The colours of a harmony around `base`, the base first. Hue-based harmonies keep the base's\n * saturation and value and turn it around the wheel; `analogous` spreads by `spread` degrees\n * (default 30); `monochromatic` keeps the hue and steps the lightness.\n */\nexport function colorHarmony(base: string, kind: HarmonyKind, options: { wheel?: ColorWheel; spread?: number } = {}): HarmonyColor[] {\n const wheel = options.wheel ?? 'ryb';\n const [r, g, b] = parseHexColor(base, 'base');\n const hsv = rgbToHsv(r, g, b);\n const start = hueToWheel(hsv.h, wheel);\n const at = (angle: number): HarmonyColor => {\n const a = ((angle % 360) + 360) % 360;\n const [rr, gg, bb] = hsvToRgb(wheelToHue(a, wheel), hsv.s, hsv.v);\n return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(a * 10) / 10 };\n };\n const spread = options.spread ?? 30;\n const offsets: Record<Exclude<HarmonyKind, 'monochromatic'>, number[]> = {\n complementary: [0, 180],\n 'split-complementary': [0, 180 - spread, 180 + spread],\n analogous: [0, -spread, spread],\n triadic: [0, 120, 240],\n tetradic: [0, 60, 180, 240],\n square: [0, 90, 180, 270],\n };\n if (kind === 'monochromatic') {\n const hsl = rgbToHsl(r, g, b);\n const steps = [0, -0.3, -0.15, 0.15, 0.3];\n return steps.map((d) => {\n const [rr, gg, bb] = d === 0 ? [r, g, b] : hslToRgb(hsl.h, hsl.s, Math.min(0.97, Math.max(0.03, hsl.l + d)));\n return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(start * 10) / 10 };\n });\n }\n return offsets[kind].map((o, i) => (i === 0 ? { color: rgbToHexColor(r, g, b), angle: Math.round(start * 10) / 10 } : at(start + o)));\n}\n\nexport interface Swatch {\n /** #rrggbb (for a gradient swatch: its first colour, what plain colour slots show). */\n color: string;\n name?: string;\n /** Gradient paint held by the swatch (`setPaint` picks it); missing for a colour swatch. */\n paint?: PaintGradientSource;\n}\n\n/** A swatch read from JSON, or `null` when it is not one (a gradient swatch keeps its gradient). */\nexport function readSwatch(raw: unknown): Swatch | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const g = readPaintSource(r.paint);\n const color = typeof r.color === 'string' && /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : g?.type === 'gradient' ? paintSourceColor(g) : null;\n if (!color) return null;\n const out: Swatch = { color };\n if (typeof r.name === 'string' && r.name.trim()) out.name = r.name.trim().slice(0, 80);\n if (g?.type === 'gradient') out.paint = g;\n return out;\n}\n\n/** A starting palette of our own generic paint colours. */\nexport const DEFAULT_SWATCHES: readonly Swatch[] = [\n { color: '#f7f5ee', name: 'Warm white' },\n { color: '#f3d23a', name: 'Lemon yellow' },\n { color: '#c8912f', name: 'Yellow ochre' },\n { color: '#d23a2a', name: 'Warm red' },\n { color: '#9e1f3d', name: 'Crimson' },\n { color: '#8a4a2b', name: 'Burnt earth' },\n { color: '#2f3f9e', name: 'Deep blue' },\n { color: '#1d6f8f', name: 'Cool blue' },\n { color: '#4f6e2c', name: 'Leaf green' },\n { color: '#25221f', name: 'Warm black' },\n];\n","/**\n * The mixing pad: a small canvas beside the painting to put paint on, smear it with a palette\n * knife and pick the mixed colour from. Paint is laid with the Mixer Brush in pigment mode and the\n * knife smears it with pigment mixing, so blue and yellow pushed together make green. It is\n * scratch space: not part of the painting's undo history, but saved with the document, and pads\n * can be kept by name (`MixingPad.toPng` / `fromPng`).\n *\n * `mix` puts measured blobs of paint down (areas in proportion to their parts) and works them\n * together with the knife, the way a painter mixes a pile on the palette.\n */\n\nimport { EMPTY_MIXER_LOAD } from '../paint/mixer';\nimport { mixPigments } from '../paint/pigment';\nimport { base64ToBytes, decodePng, isPng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport { emptySurface, readRegion, surfaceFromPixels, writeRegion, type TiledSurface } from '../raster/surface';\nimport { normalizePaintBrush, type PaintBrush } from './brushes';\nimport { IDENTITY_COPY, type PenPoint } from './engine';\nimport { hexToRgb8 } from './paper';\nimport { brushLoadForStroke, PaintStrokeSession } from './stroke';\n\nexport type MixingPadTool = 'paint' | 'knife';\n\nexport const MIXING_PAD_WIDTH = 360;\nexport const MIXING_PAD_HEIGHT = 220;\n\nconst PAD_PAINT: PaintBrush = normalizePaintBrush({\n id: 'pad-paint',\n name: 'Pad paint',\n category: 'mixer',\n engine: 'mixer',\n tip: { size: 28, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 0.7, pressureSize: false, pressureOpacity: false },\n mixer: { wet: 0.45, load: 1, mix: 0.4, loadEachStroke: true, cleanEachStroke: true, mixing: 'pigment' },\n});\n\nconst PAD_KNIFE: PaintBrush = normalizePaintBrush({\n id: 'pad-knife',\n name: 'Palette knife',\n category: 'smudge',\n engine: 'smudge',\n loadsColor: false,\n tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow: 1, pressureSize: false, pressureOpacity: false },\n // A knife drags streaks of paint and folds them into each other, mixing them as pigment.\n smudge: { strength: 0.6, length: 0.8, mixing: 'pigment' },\n});\n\n/** A solid round blob of paint (one dab). */\nconst PAD_BLOB: PaintBrush = normalizePaintBrush({\n id: 'pad-blob',\n name: 'Pad blob',\n category: 'custom',\n engine: 'dab',\n tip: { size: 20, hardness: 1, spacing: 0.1, opacity: 1, flow: 1, pressureSize: false, pressureOpacity: false },\n});\n\n/** One paint in a `mix`: a colour and its parts. */\nexport interface PadMixPart {\n color: string;\n parts: number;\n}\n\nexport interface PadMixOptions {\n /** Centre of the pile in pad pixels (default the pad's centre). */\n x?: number;\n y?: number;\n /** Radius of the pile in pad pixels, 12..100 (default 40). */\n radius?: number;\n /** Knife passes over the pile, 1..20 (default 3): more passes, finer streaks around the mixed core. */\n passes?: number;\n /** Wipe the pile's area first (default true); false mixes into what is already there. */\n clear?: boolean;\n}\n\nexport class MixingPad {\n readonly width = MIXING_PAD_WIDTH;\n readonly height = MIXING_PAD_HEIGHT;\n private surface: TiledSurface = emptySurface(4);\n /** Increments on every change (for redrawing). */\n version = 0;\n\n /** Paints (`paint`, with `color`) or smears (`knife`) through `points` in pad pixels. */\n stroke(points: readonly PenPoint[], options: { tool: MixingPadTool; color: string; size?: number }): { bounds: { x: number; y: number; width: number; height: number } | null } {\n if (!points.length) throw new Error('points is empty; give at least one {x, y} in pad pixels.');\n for (const [i, p] of points.entries()) {\n if (!Number.isFinite(p.x) || !Number.isFinite(p.y) || p.x < 0 || p.y < 0 || p.x > this.width || p.y > this.height) {\n throw new Error(`points[${i}] (${p.x}, ${p.y}) is off the ${this.width}×${this.height} mixing pad.`);\n }\n }\n let brush = options.tool === 'knife' ? PAD_KNIFE : PAD_PAINT;\n if (options.size !== undefined) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);\n return this.run(brush, points, options.color);\n }\n\n /** Paints `points` with `brush` in `color` on the pad. */\n private run(brush: PaintBrush, points: readonly PenPoint[], color: string): { bounds: { x: number; y: number; width: number; height: number } | null } {\n const [r, g, b] = hexToRgb8(color);\n let result: TiledSurface | null = null;\n const session = new PaintStrokeSession(\n { update: () => {}, commit: (_s, s) => (result = s) },\n {\n layerId: 'mixing-pad',\n surface: this.surface,\n offset: { x: 0, y: 0 },\n lockTransparency: false,\n brush,\n color: [r, g, b, 255],\n background: [255, 255, 255, 255],\n grain: null,\n paperDepth: 0,\n seed: 1,\n colorHex: color,\n affineCopies: [],\n copies: [IDENTITY_COPY],\n mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),\n }\n );\n session.push(points);\n const res = session.end();\n if (result) {\n this.surface = result;\n this.version++;\n }\n return { bounds: res.bounds };\n }\n\n /** The average colour within `radius` pixels of (x, y), or `null` where the pad is bare. */\n pick(x: number, y: number, radius = 3): string | null {\n if (!(x >= 0 && y >= 0 && x < this.width && y < this.height)) throw new Error(`(${x}, ${y}) is off the ${this.width}×${this.height} mixing pad.`);\n const r = Math.max(0, Math.round(radius));\n const x0 = Math.max(0, Math.floor(x) - r);\n const y0 = Math.max(0, Math.floor(y) - r);\n const x1 = Math.min(this.width, Math.floor(x) + r + 1);\n const y1 = Math.min(this.height, Math.floor(y) + r + 1);\n const px = readRegion(this.surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });\n let R = 0;\n let G = 0;\n let B = 0;\n let A = 0;\n for (let i = 0; i < px.length; i += 4) {\n const a = px[i + 3];\n R += px[i] * a;\n G += px[i + 1] * a;\n B += px[i + 2] * a;\n A += a;\n }\n if (A === 0) return null;\n const hex = (v: number) => Math.round(v / A).toString(16).padStart(2, '0');\n return `#${hex(R)}${hex(G)}${hex(B)}`;\n }\n\n clear(): void {\n this.surface = emptySurface(4);\n this.version++;\n }\n\n /**\n * Mixes paints the way a painter does on a palette: blobs whose areas are in proportion to\n * their parts, side by side around (x, y), spiral knife passes that fold them together, and then the centre worked until it is fully mixed: a soft-edged core\n * of the Kubelka-Munk mix of the parts (`mixPigments`), with partly mixed streaks around it.\n * Returns that colour (picked back from the centre) and the pile's bounds.\n *\n * The core is laid, not smeared, because smearing alone drifts: the knife samples in RGB and a\n * mixed colour's scattering is re-estimated from its luminance, so repeated small mixes favour\n * the lighter paint (see GOTCHAS).\n */\n mix(paints: readonly PadMixPart[], options: PadMixOptions = {}): { color: string | null; bounds: { x: number; y: number; width: number; height: number } } {\n if (!paints.length || paints.length > 8) throw new Error(`Give 1 to 8 paints to mix; got ${paints.length}.`);\n const total = paints.reduce((n, p) => n + p.parts, 0);\n paints.forEach((p, i) => {\n if (!(Number.isFinite(p.parts) && p.parts >= 0)) throw new Error(`paints[${i}].parts is ${p.parts}; use a number ≥ 0.`);\n });\n if (!(total > 0)) throw new Error('Every paint has 0 parts; give at least one paint parts above 0.');\n const R = options.radius ?? 40;\n if (!(R >= 12 && R <= 100)) throw new Error(`radius ${R} is out of range; use 12..100 pad px.`);\n const passes = options.passes ?? 3;\n if (!(Number.isInteger(passes) && passes >= 1 && passes <= 20)) throw new Error(`passes ${passes} is out of range; use a whole number 1..20.`);\n const cx = options.x ?? this.width / 2;\n const cy = options.y ?? this.height / 2;\n if (!(cx - R >= 0 && cy - R >= 0 && cx + R <= this.width && cy + R <= this.height)) {\n throw new Error(`A pile of radius ${R} at (${cx}, ${cy}) does not fit on the ${this.width}×${this.height} pad; move it at least ${R} px from every edge or use a smaller radius.`);\n }\n const bounds = { x: Math.floor(cx - R), y: Math.floor(cy - R), width: Math.ceil(2 * R), height: Math.ceil(2 * R) };\n if (options.clear !== false) this.surface = writeRegion(this.surface, bounds, new Uint8ClampedArray(bounds.width * bounds.height * 4));\n // Blobs: equal-area share of a disc of radius 0.7·R per part, placed around the centre.\n const used = paints.filter((p) => p.parts > 0);\n const area = Math.PI * (0.7 * R) ** 2;\n used.forEach((p, i) => {\n const r = Math.sqrt((area * (p.parts / total)) / Math.PI);\n const a = (i / used.length) * Math.PI * 2;\n const off = used.length === 1 ? 0 : 0.45 * R;\n const x = Math.min(cx + R - r, Math.max(cx - R + r, cx + Math.cos(a) * off));\n const y = Math.min(cy + R - r, Math.max(cy - R + r, cy + Math.sin(a) * off));\n const blob = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: Math.max(2, 2 * r) } }, PAD_BLOB);\n this.run(blob, [{ x, y }], p.color.toLowerCase());\n });\n // Knife passes: spiral stirs through the pile, each turned a little, folding the blobs together.\n const knife = normalizePaintBrush({ tip: { ...PAD_KNIFE.tip, size: Math.max(12, R * 0.5) } }, PAD_KNIFE);\n for (let k = 0; k < passes; k++) {\n const stir: PenPoint[] = [];\n const turns = 1.6;\n for (let t = 0; t <= 48; t++) {\n const a = (t / 48) * Math.PI * 2 * turns + k * 1.3;\n const rr = R * (0.85 - 0.6 * (t / 48));\n stir.push({ x: cx + Math.cos(a) * rr, y: cy + Math.sin(a) * rr });\n }\n this.run(knife, stir, '#000000');\n }\n const mixed = mixPigments(\n paints.map((p) => hexToRgb8(p.color).map((v) => v / 255) as [number, number, number]),\n paints.map((p) => p.parts)\n );\n const core = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: R * 1.1, hardness: 0.55 } }, PAD_BLOB);\n const hex = '#' + mixed.map((v) => Math.round(v * 255).toString(16).padStart(2, '0')).join('');\n this.run(core, [{ x: cx, y: cy }], hex);\n return { color: this.pick(cx, cy, 2), bounds };\n }\n\n /** The pad's pixels as a PNG data URL (with alpha), for saving. */\n toPng(): string {\n return pngDataUrl(this.width, this.height, readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height }));\n }\n\n /** Replaces the pad with a PNG (data URL or base64) of the pad's size, as `toPng` writes. */\n fromPng(png: string): void {\n const bytes = png.startsWith('data:') ? parseDataUrl(png)?.bytes : base64ToBytes(png);\n if (!bytes || !isPng(bytes)) throw new Error('The pad image is not a PNG data URL or base64 PNG.');\n const img = decodePng(bytes);\n if (img.width !== this.width || img.height !== this.height) throw new Error(`The pad image is ${img.width}×${img.height}; a mixing pad is ${this.width}×${this.height}.`);\n this.surface = surfaceFromPixels({ width: img.width, height: img.height, data: img.data }, 4);\n this.version++;\n }\n\n /** Whether nothing is on the pad. */\n get empty(): boolean {\n return this.surface.tiles.size === 0;\n }\n\n /** The pad over white, `width × height` RGBA. */\n pixels(): Uint8ClampedArray {\n const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });\n for (let i = 0; i < px.length; i += 4) {\n const a = px[i + 3] / 255;\n px[i] = Math.round(px[i] * a + 255 * (1 - a));\n px[i + 1] = Math.round(px[i + 1] * a + 255 * (1 - a));\n px[i + 2] = Math.round(px[i + 2] * a + 255 * (1 - a));\n px[i + 3] = 255;\n }\n return px;\n }\n}\n","/**\n * Replaying a painting from its data: every recorded stroke, and the changes recorded between them\n * (fills, clears, flattened relief, and wet-media events at the simulation clock they happened at),\n * repainted into a fresh, headless editor.\n *\n * - **Time-lapse**: frames of the painting as it is repainted (after strokes, and while wet paint\n * flows between them), as PNGs and as a `VideoProject` of stills that the video editor's export\n * path (`toVideoFfmpeg`) turns into an MP4.\n * - **High-resolution export**: the same replay at a larger scale (points, brush sizes, paper grain\n * and rectangles scaled), so the picture is painted again with more pixels instead of upscaled.\n *\n * Strokes paint exactly as recorded: same seed, load and symmetry copies, and wet strokes apply\n * each point at the clock it was recorded at, with the simulation stepped in between, so wet paint\n * flows as it did. What was not recorded as data (imported pixels, merges, transforms of layers,\n * changes made before strokes were recorded) is not repeated: those layers start empty.\n */\n\nimport { encodePng } from '../raster/png';\nimport { writeZip } from '../export/zip-writer';\nimport { emptySurface } from '../raster/surface';\nimport { mapLayer, flattenLayers } from '../raster/layers';\nimport type { VideoAsset, VideoClip, VideoProject } from '../video-editor/types';\nimport { toVideoFfmpeg, type VideoFfmpegExport } from '../video-editor/export';\nimport type { PaintDocumentState, PaintLayer, PaintPixelLayer } from './document';\nimport type { PaintEditorController, PixelImage } from './controller';\nimport type { PenPoint } from './engine';\nimport { IDENTITY_AFFINE, varyStroke, type Affine, type RecordedStroke } from './strokes';\nimport type { WetEvent } from './wet';\nimport { scaleStencil } from './stencils';\nimport type { Sticker } from './stickers';\nimport type { WetFlowSpec } from '../paint/wet/flow';\nimport type { WetParams, WetTilt } from '../paint/wet/model';\nimport { downsampleRgba } from './render';\n\nexport interface PaintReplayOptions {\n /** Paint at this many times the size (1 for a time-lapse; 1..8 for a high-resolution export). */\n scale?: number;\n /** Stop after this many strokes (default all). */\n strokes?: number;\n}\n\n/** One frame of a replay. */\nexport interface ReplayFrame extends PixelImage {\n /** Strokes painted so far. */\n stroke: number;\n /** Wet-media clock (steps). */\n clock: number;\n}\n\nexport interface TimeLapseOptions {\n /** Longest side of each frame in pixels (default 720). */\n maxSide?: number;\n /** A frame after every this many strokes (default: about `frames` frames over the whole painting). */\n strokesPerFrame?: number;\n /** About how many frames in all (default 120). */\n frames?: number;\n /** While wet paint flows between strokes, a frame every this many steps (default 60; 0: none). */\n wetStepsPerFrame?: number;\n /** Frames per second of the video (default 12). */\n fps?: number;\n /** Hold the finished painting this long at the end (ms, default 1500). */\n holdMs?: number;\n /** Called after each frame with how far along the replay is (0..1). */\n onProgress?: (done: number) => void;\n}\n\nexport interface TimeLapse {\n fps: number;\n width: number;\n height: number;\n /** Each frame as PNG bytes. */\n frames: Uint8Array[];\n /** The frames as stills on a timeline (assets `frame-0001.png`…; `src` is that file name). */\n video: VideoProject;\n /** The ffmpeg command that renders `video` to `timelapse.mp4` from the frame files. */\n ffmpeg: VideoFfmpegExport;\n durationMs: number;\n}\n\ntype Item = { kind: 'stroke'; stroke: RecordedStroke } | { kind: 'wet'; event: WetEvent } | { kind: 'paint'; event: NonNullable<PaintDocumentState['paintEvents']>[number] };\n\n/** Strokes and events in the order they happened. */\nexport function replayItems(doc: PaintDocumentState): Item[] {\n const items: Array<Item & { at: number; order: number }> = [];\n let order = 0;\n doc.strokes.forEach((stroke, i) => items.push({ kind: 'stroke', stroke, at: i + 0.5, order: order++ }));\n for (const event of doc.paintEvents ?? []) items.push({ kind: 'paint', event, at: event.at, order: order++ });\n for (const event of doc.wetEvents ?? []) items.push({ kind: 'wet', event, at: event.at ?? 0, order: order++ });\n items.sort((a, b) => a.at - b.at || a.order - b.order);\n return items.map(({ at: _a, order: _o, ...item }) => item as Item);\n}\n\n/** The layer tree with every pixel layer empty (what a replay paints into). */\nfunction emptiedLayers(layers: readonly PaintLayer[]): PaintLayer[] {\n let out = [...layers] as PaintLayer[];\n for (const l of flattenLayers(layers)) if (l.type === 'pixel') out = mapLayer(out, l.id, () => ({ ...(l as PaintPixelLayer), surface: emptySurface(4), offset: { x: 0, y: 0 } }));\n return out;\n}\n\n/**\n * Replays a controller's painting into `target` (a fresh controller the caller made at the scaled\n * size; see `PaintEditorController.createReplayTarget`), calling `onFrame` where frames are due.\n */\nexport async function replayPainting(\n source: PaintDocumentState,\n target: PaintEditorController,\n options: PaintReplayOptions & { onStroke?: (index: number) => Promise<void> | void; onWetSteps?: (clock: number) => Promise<void> | void; wetChunk?: number } = {}\n): Promise<void> {\n const s = options.scale ?? 1;\n const scaleM: Affine = [s, 0, 0, s, 0, 0];\n const items = replayItems(source);\n const maxStrokes = options.strokes ?? source.strokes.length;\n const chunk = Math.max(1, options.wetChunk ?? 0) || Infinity;\n let strokes = 0;\n const advanceTo = async (clock: number) => {\n const now = target.getState().wet.clock;\n let left = clock - now;\n while (left > 0) {\n const n = Math.min(left, chunk);\n await target.advanceWet({ steps: n });\n left -= n;\n if (Number.isFinite(chunk)) await options.onWetSteps?.(target.getState().wet.clock);\n }\n };\n const sr = (r: { x: number; y: number; width: number; height: number }) => ({ x: r.x * s, y: r.y * s, width: r.width * s, height: r.height * s });\n for (const item of items) {\n if (strokes >= maxStrokes && item.kind === 'stroke') break;\n if (item.kind === 'paint') {\n const e = item.event;\n if (!target.getLayer(e.layerId)) continue;\n if (e.kind === 'fill') target.fillLayer({ layerId: e.layerId, ...(e.data as object) });\n else if (e.kind === 'clear') target.clearLayer(e.layerId);\n else if (e.kind === 'stickers') {\n const list = ((e.data as { stickers?: Sticker[] })?.stickers ?? []).map((x) => (s === 1 ? x : { ...x, x: x.x * s, y: x.y * s, size: x.size * s }));\n target.replaceStickers(e.layerId, list);\n } else target.flattenRelief(e.layerId);\n continue;\n }\n if (item.kind === 'wet') {\n const e = item.event;\n await target.wetIdle();\n await advanceTo(e.clock);\n try {\n switch (e.kind) {\n case 'dry': {\n const rect = e.data as { x: number; y: number; width: number; height: number } | undefined;\n const ids = e.layerId?.split(',').filter(Boolean);\n if (ids?.length) for (const id of ids) target.dryPaint({ layerId: id, rect: rect ? sr(rect) : undefined });\n else target.dryPaint({ rect: rect ? sr(rect) : undefined });\n break;\n }\n case 'tilt':\n target.setCanvasTilt(e.data as WetTilt);\n break;\n case 'params':\n if (e.layerId && target.getLayer(e.layerId)) target.setWetParams(e.layerId, e.data as WetParams);\n break;\n case 'flow': {\n const f = { ...(e.data as WetFlowSpec) };\n if (f.center) f.center = { x: f.center.x * s, y: f.center.y * s };\n if (f.scale) f.scale = Math.min(1024, f.scale * s);\n if (e.layerId && target.getLayer(e.layerId)) target.setFlowMap(e.layerId, f);\n break;\n }\n case 'wet-paper': {\n const d = e.data as { rect: { x: number; y: number; width: number; height: number }; amount: number };\n if (e.layerId && target.getLayer(e.layerId)) target.wetPaper({ layerId: e.layerId, rect: sr(d.rect), amount: d.amount });\n break;\n }\n }\n } catch {\n // An event that no longer applies (its layer went away) is skipped.\n }\n continue;\n }\n const st = s === 1 ? item.stroke : varyStroke(item.stroke, scaleM, { scale: s });\n strokes++;\n if (!target.getLayer(st.layerId)) continue;\n const init = { layerId: st.layerId, color: st.color, brush: st.brush, seed: st.seed, copies: (st.copies ?? []) as Affine[], ...(st.load ? { load: st.load } : {}), ...(st.paint ? { paint: st.paint } : {}), snapToPerspective: false, stencils: false, maskStencils: (st.stencils ?? []).map((x) => (s === 1 ? x : scaleStencil(x, s))) };\n if (st.brush.engine === 'wet' && st.steps?.length === st.points.length) {\n // Each point at the clock it was recorded at, the simulation stepped in between.\n await target.wetIdle();\n await advanceTo(st.steps[0]);\n const session = target.beginStroke({ ...init, start: st.points[0] });\n for (let i = 0; i < st.points.length; i++) {\n await advanceTo(st.steps[i]);\n await target.wetIdle();\n session.push([st.points[i] as PenPoint]);\n }\n await target.wetIdle();\n session.end();\n while (session.pendingEnd) {\n await target.wetIdle();\n session.completeWetEnd();\n if (session.pendingEnd) await new Promise((r) => setTimeout(r, 0));\n }\n } else {\n await target.wetIdle();\n target.paintStroke({ ...init, points: st.points });\n }\n await options.onStroke?.(strokes);\n }\n void IDENTITY_AFFINE;\n}\n\n/** A frame of `target` at most `maxSide` pixels on its longest side. */\nexport function replayFrame(target: PaintEditorController, maxSide: number, stroke: number): ReplayFrame {\n const img = target.renderPreview({ maxSide, wet: 'display' });\n return { ...img, stroke, clock: target.getState().wet.clock };\n}\n\n/** Frames as stills on a video timeline, `1 / fps` each, with the last held for `holdMs`. */\nexport function timeLapseVideo(frameCount: number, width: number, height: number, fps: number, holdMs: number, name = 'Time-lapse'): { video: VideoProject; ffmpeg: VideoFfmpegExport; durationMs: number } {\n const w = Math.max(2, 2 * Math.round(width / 2));\n const h = Math.max(2, 2 * Math.round(height / 2));\n const frameMs = 1000 / fps;\n const assets: VideoAsset[] = [];\n const clips: VideoClip[] = [];\n for (let i = 0; i < frameCount; i++) {\n const id = `frame-${String(i + 1).padStart(4, '0')}`;\n assets.push({ id, kind: 'image', src: `${id}.png`, name: `${id}.png`, width, height });\n const start = Math.round(i * frameMs * 1000) / 1000;\n const end = i === frameCount - 1 ? start + frameMs + holdMs : Math.round((i + 1) * frameMs * 1000) / 1000;\n clips.push({ id: `clip-${i + 1}`, type: 'media', asset: id, start, duration: end - start } as VideoClip);\n }\n const video: VideoProject = { version: 1, name, width: w, height: h, fps, background: '#000000', assets, tracks: [{ id: 'v1', kind: 'video', clips } as VideoProject['tracks'][number]] };\n const durationMs = frameCount ? clips[clips.length - 1].start + clips[clips.length - 1].duration : 0;\n return { video, ffmpeg: toVideoFfmpeg(video, { output: 'timelapse.mp4' }), durationMs };\n}\n\n/** Builds the time-lapse of a painting (see `PaintEditorController.timeLapse`). */\nexport async function buildTimeLapse(source: PaintDocumentState, target: PaintEditorController, options: TimeLapseOptions = {}): Promise<TimeLapse> {\n const maxSide = Math.max(32, Math.min(4096, Math.round(options.maxSide ?? 720)));\n const total = source.strokes.length;\n const perFrame = Math.max(1, options.strokesPerFrame ?? Math.ceil(total / Math.max(1, options.frames ?? 120)));\n const wetEvery = options.wetStepsPerFrame ?? 60;\n const fps = Math.max(1, Math.min(60, options.fps ?? 12));\n const frames: Uint8Array[] = [];\n let width = 0;\n let height = 0;\n const shoot = (stroke: number) => {\n const f = replayFrame(target, maxSide, stroke);\n width = f.width;\n height = f.height;\n frames.push(encodePng(f.width, f.height, f.data));\n };\n shoot(0);\n await replayPainting(source, target, {\n wetChunk: wetEvery > 0 ? wetEvery : 0,\n onStroke: (i) => {\n if (i % perFrame === 0 || i === total) shoot(i);\n options.onProgress?.(total ? i / total : 1);\n },\n onWetSteps: wetEvery > 0 ? () => shoot(-1) : undefined,\n });\n // Let the last wet paint settle on screen.\n const { video, ffmpeg, durationMs } = timeLapseVideo(frames.length, width, height, fps, options.holdMs ?? 1500);\n return { fps, width, height, frames, video, ffmpeg, durationMs };\n}\n\n/** A contact sheet: up to `count` frames side by side, for a quick look. */\nexport function contactSheet(frames: readonly PixelImage[], columns = 3, cell = 200): PixelImage {\n const n = frames.length;\n const rows = Math.ceil(n / columns);\n if (n) cell = Math.min(cell, frames[0].width);\n const ar = n ? frames[0].height / frames[0].width : 1;\n const ch = Math.max(1, Math.round(cell * ar));\n const out = new Uint8ClampedArray(columns * cell * rows * ch * 4).fill(255);\n const W = columns * cell;\n frames.forEach((f, i) => {\n const small = downsampleRgba(f.data, f.width, f.height, cell, ch);\n const ox = (i % columns) * cell;\n const oy = Math.floor(i / columns) * ch;\n for (let y = 0; y < ch; y++) out.set(small.subarray(y * cell * 4, (y + 1) * cell * 4), ((oy + y) * W + ox) * 4);\n });\n return { width: W, height: rows * ch, data: out };\n}\n\nexport { emptiedLayers };\n\n/**\n * A time-lapse as one ZIP: the frames (`frame-0001.png`…), the video project (`timelapse.json`,\n * which the video editor opens), and `render.sh` with the ffmpeg command that turns them into\n * `timelapse.mp4`.\n */\nexport async function timeLapseZip(tl: TimeLapse): Promise<Uint8Array> {\n const entries = tl.frames.map((data, i) => ({ name: `frame-${String(i + 1).padStart(4, '0')}.png`, data }));\n entries.push({ name: 'timelapse.json', data: new TextEncoder().encode(JSON.stringify(tl.video, null, 2)) });\n entries.push({ name: 'render.sh', data: new TextEncoder().encode(`#!/bin/sh\\n# Renders the time-lapse with ffmpeg (run in this folder).\\n${tl.ffmpeg.command}\\n`) });\n return writeZip(entries, { compression: 'store' });\n}\n","/**\n * `PaintEditorController`: the natural-media paint editor's state, framework-free.\n *\n * - The document (canvas, paper, the shared raster layer stack, recorded strokes) is immutable\n * and tiled copy-on-write; each undo step is a reference to a whole document state.\n * - Strokes: `beginStroke` gives a live session for pointer input (one undo step at `end`);\n * `paintStroke` paints a whole stroke from data (agents, tests, replays).\n * - The brush, colour, Mixer Brush load, view and hand are editor state, not undo steps.\n * - Rendering goes through the shared compositor with the paper as its bottom layer; exports\n * are PNG (pure TypeScript), JPEG / WebP (through a canvas or a host encoder) and PSD (through\n * the shared PSD writer, with the paper as a pixel layer).\n */\n\nimport { exportPsd, type PsdExportResult } from '../image-editor/psd/export';\nimport type { ImageLayer } from '../image-editor/layers/model';\nimport { applyStrokeToTile, CoverageMap, type StrokeApply } from '../paint/brush';\nimport { cleanMixer, EMPTY_MIXER_LOAD, loadMixer, type MixerLoad } from '../paint/mixer';\nimport { paintSourceColor, parsePaintSource, PaintSourceError, resolvePaintGradient, type PaintGradientSource, type PaintSource } from '../paint/gradient-paint';\nimport { BLEND_MODES, isBlendMode, standardBlendMode, type BlendMode } from '../raster/blend';\nimport { findLayer, flattenLayers, insertLayer, isLayerLocked, mapLayer, type LayerLocks } from '../raster/layers';\nimport { bytesToBase64, decodePng, encodePng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport {\n translateSurface,\n contentBounds,\n emptySurface,\n readPixel,\n surfaceFromPixels,\n TILE,\n TiledSurface,\n tileKey,\n tilesInRect,\n finishTile,\n type RasterRect,\n type Tile,\n} from '../raster/surface';\nimport { brushToJSON, BUILTIN_BRUSHES, DEFAULT_BRUSH_ID, normalizePaintBrush, type PaintBrushInput, parseBrushLibrary, BRUSH_ID, type BrushLibraryFile, type PaintBrush } from './brushes';\nimport {\n createPaintDocument,\n createPaintLayer,\n newPaintLayerId,\n normalizeCanvas,\n type PaintCanvas,\n type PaintDocumentState,\n type PaintLayer,\n type PaintPixelLayer,\n type PaintEvent,\n} from './document';\nimport { defaultImageEncoder, type ImageEncoder, type ImageFormat } from './encode';\nimport type { PenPoint } from './engine';\nimport type { Hand } from './input';\nimport { mix32 } from './noise';\nimport { clonePaper, grainCatch, hexToRgb8, normalizePaper, paperPreset, type PaperSettings } from './paper';\nimport { createPaintCompositor, downsampleRgba, paperGrain, paperNode, TRACING_NODE_ID, type ImageNode, type ViewLayer } from './render';\nimport {\n deserializePaintDocument,\n deserializePaintDocumentAsync,\n serializePaintDocument,\n serializePaintDocumentExternal,\n type PaintAssetStore,\n type PaintDocumentExtensions,\n type SerializedPaintDocument,\n} from './serialize';\nimport { brushLoadForStroke, PaintStrokeSession, type PaintStrokeResult, type StrokeSessionHost } from './stroke';\nimport { affineCopy, strokeBounds, variationAffine, varyStroke, IDENTITY_AFFINE, type Affine, type RecordedStroke, type StrokeVariation } from './strokes';\nimport { renderBrushThumbnail, type BrushThumbnail } from './thumbnails';\nimport { colorHarmony, DEFAULT_SWATCHES, readSwatch, type ColorWheel, type HarmonyColor, type HarmonyKind, type Swatch } from './color';\nimport {\n defaultGuides,\n normalizePerspective,\n normalizeSymmetry,\n PerspectiveSnapper,\n symmetryCopies,\n type PaintGuides,\n type PerspectiveGuide,\n type ReferenceImage,\n type SymmetryGuide,\n type TracingImage,\n} from './guides';\nimport { MixingPad, type MixingPadTool, type PadMixOptions } from './mixing-pad';\nimport { normalizeStencil, StencilSnapper, stencilMask, type Stencil, type StencilInput } from './stencils';\nimport { normalizeStickerUpdate, STAMP_KINDS, stickerSurface, type Sticker } from './stickers';\nimport { DEFAULT_VIEW, normalizeView, type PaintView } from './view';\nimport { brushTakesPaint } from './brushes';\nimport { PaintWetLayers, WET_GPU_MODES, WetStrokeDriver, type PaintWetOptions, type WetEngineInfo, type WetEvent, type WetGpuMode, type WetLayerInfo, type WetLayerSettings } from './wet';\nimport { isWetBrush } from './brushes';\nimport { normalizeWetParams, normalizeTilt, type WetParams, type WetTilt } from '../paint/wet/model';\nimport { normalizeFlow, type WetFlowSpec } from '../paint/wet/flow';\nimport { MAX_WET_LANES, type WetBackendInfo, type WetSnapshot } from '../paint/wet/world';\nimport { wetSupport } from '../paint/wet/wasm';\nimport { buildTimeLapse, emptiedLayers, replayPainting, type TimeLapse, type TimeLapseOptions } from './replay';\nimport { DEFAULT_LIGHTING, emptyThickChannels, LitCache, normalizeLighting, reliefInfo, thickFromJSON, thickToJSON, type PaintLighting, type ThickChannels } from './thick';\n\n/** A refusal with a message that says what is wrong and how to fix it. */\nexport class PaintEditorError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PaintEditorError';\n }\n}\n\nexport interface PaintEditorOptions extends Partial<PaintCanvas> {\n /** Paper settings, or a library preset id (default cold pressed). */\n paper?: Partial<PaperSettings> | string;\n /** Active brush id (default `pencil-6b`). */\n brushId?: string;\n /** Saved user brushes to start the library with. */\n brushes?: readonly PaintBrush[];\n /** Paint colour #rrggbb (default #1f2937). */\n color?: string;\n hand?: Hand;\n /** Encodes JPEG / WebP (default: a canvas, when there is one). */\n encodeImage?: ImageEncoder;\n /** Undo steps kept (default 200). */\n historyLimit?: number;\n /** Wet media: kernel, threads, speed (steps per second) and the memory kept for undoing wet strokes. */\n wet?: PaintWetOptions;\n}\n\n/** Wet media as panels show it. */\nexport interface PaintWetStateInfo {\n /** Layers with a wet layer (paint still wet or drying). */\n layerIds: string[];\n /** Some paint is wet. */\n wet: boolean;\n /** Time is stopped (the paint stays wet). */\n paused: boolean;\n /** The water overlay is shown. */\n overlay: boolean;\n /** Wet strokes dry as soon as they end. */\n instantDry: boolean;\n /** Steps per second while time runs. */\n speed: number;\n /** Simulation steps so far. */\n clock: number;\n tilt: WetTilt;\n /** Engine B (WebGPU): `auto` (used for large wet areas when there is a GPU) or `off` (CPU only). */\n gpu: WetGpuMode;\n /** Engine B is shading the wet paint now. */\n gpuActive: boolean;\n}\n\nexport interface PaintEditorState {\n document: PaintDocumentState;\n /** The active brush (possibly edited from its preset). */\n brush: PaintBrush;\n /** The brush library: built-in presets, then the user's brushes. */\n brushes: readonly PaintBrush[];\n /** Paint colour #rrggbb. */\n color: string;\n /** The second colour (swap with `swapColors`). */\n secondaryColor: string;\n /**\n * What strokes paint with: the paint colour, or gradient paint (`setPaint`). A brush with its\n * own gradient (`brush.paint`) paints that instead.\n */\n paint: PaintSource;\n /** What the Mixer Brush carries between strokes. */\n mixerLoad: MixerLoad;\n view: PaintView;\n hand: Hand;\n canUndo: boolean;\n canRedo: boolean;\n undoLabel: string | null;\n redoLabel: string | null;\n /** Mirror / kaleidoscope, perspective, tracing image and reference images (saved with the document, not undo steps). */\n guides: PaintGuides;\n /** The colour swatches (saved with the document, not undo steps). */\n swatches: readonly Swatch[];\n /** Changes whenever the mixing pad does. */\n mixingPadVersion: number;\n /** Mixing pads kept by name (saved with the document). */\n savedMixingPads: readonly SavedMixingPadInfo[];\n /** A stroke is in progress. */\n stroking: boolean;\n /** Wet media. */\n wet: PaintWetStateInfo;\n /** Increments on every change. */\n version: number;\n}\n\n/** A layer as tools and panels describe it. */\nexport interface PaintLayerInfo {\n id: string;\n name: string;\n type: 'pixel' | 'group';\n visible: boolean;\n opacity: number;\n blend: string;\n clipped: boolean;\n locks: LayerLocks;\n active: boolean;\n /** Painted content (document pixels), or `null` when empty. */\n bounds: RasterRect | null;\n parentId: string | null;\n depth: number;\n}\n\nexport interface PaintStrokeInit {\n /** Default the active layer. */\n layerId?: string;\n /** Default the editor colour. */\n color?: string;\n /**\n * What this stroke paints with, instead of the editor's paint: a colour, or gradient paint\n * (`@pixerate/editor/paint` `PaintSource`). A colour here is the same as `color`.\n */\n paint?: PaintSource | string;\n /** Default the active brush. */\n brushId?: string;\n /** Changes to the brush for this stroke only. */\n brush?: PaintBrushInput;\n /** Jitter seed (default: a new one per stroke). */\n seed?: number;\n /** Extra copies of the stroke as affine maps (default: the symmetry guide's). */\n copies?: readonly Affine[];\n /** Snap the samples to the perspective guide (default true for `beginStroke`, false for `paintStroke`). */\n snapToPerspective?: boolean;\n /** Where the stroke starts, for taking its colour from the tracing image before the first sample (`beginStroke`). */\n start?: { x: number; y: number };\n /** What the brush carries at the start (default: from the editor's brush load); replays pass the recorded one. */\n load?: MixerLoad;\n /** Follow guide stencils and keep to mask stencils (default true; replays pass false, their points already follow them). */\n stencils?: boolean;\n /** Mask with these stencils instead (replays pass the ones recorded with the stroke). */\n maskStencils?: readonly Stencil[];\n}\n\nexport interface LayerUpdate {\n name?: string;\n visible?: boolean;\n opacity?: number;\n blend?: BlendMode;\n clipped?: boolean;\n locks?: LayerLocks;\n}\n\nexport interface FillLayerOptions {\n layerId?: string;\n /** Default the editor colour. */\n color?: string;\n /** 0..1 (default 1). */\n opacity?: number;\n /** Paper grain response 0..1 (default 0: a flat fill); above 0 the fill catches the grain like pastel rubbed across the paper. */\n grain?: number;\n /** With grain: pressure 0..1, how far the fill reaches into the valleys (default 0.5). */\n pressure?: number;\n blend?: BlendMode;\n}\n\nexport interface ReplayOptions extends StrokeVariation {\n /** Recorded stroke ids (see `getStrokes`). */\n strokeIds: readonly string[];\n /** Paint on this layer instead of each stroke's own. */\n layerId?: string;\n /** Recolour (default each stroke's colour). */\n color?: string;\n /** Replay with this brush instead (default each stroke's brush). */\n brushId?: string;\n /** Repeat the variation this many times, each one applied on top of the last (default 1). */\n repeat?: number;\n}\n\nexport interface RenderOptions {\n /** Include the paper (default true); without it the result is transparent where nothing is painted. */\n paper?: boolean;\n /** Only these layers (a group with its children). */\n layerIds?: readonly string[];\n /**\n * Wet paint as it is now (`display`: still wet, wet paper a little darker) or dried where it is\n * (`dried`, the deterministic bake that saving uses). Default `dried` for exports and `display`\n * for previews and picks.\n */\n wet?: 'display' | 'dried';\n}\n\nexport interface PixelImage {\n width: number;\n height: number;\n data: Uint8ClampedArray;\n}\n\n/** A mixing pad kept by name. */\nexport interface SavedMixingPadInfo {\n id: string;\n name: string;\n}\n\ninterface SavedMixingPad extends SavedMixingPadInfo {\n /** The pad as a PNG data URL. */\n png: string;\n}\n\ninterface HistoryEntry {\n label: string;\n before: PaintDocumentState;\n after: PaintDocumentState;\n /** Wet steps: the wet simulation before and after (`lost` once dropped for memory: undo then leaves the paint dry). */\n wet?: { before: WetSnapshot | null; after: WetSnapshot | null; lost: boolean };\n}\n\nconst HEX = /^#[0-9a-f]{6}$/i;\n\nfunction parseColor(value: unknown, what = 'color'): string {\n if (typeof value !== 'string' || !HEX.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. \"#2a4b8d\".`);\n return value.trim().toLowerCase();\n}\n\nfunction rgbaOf(hex: string): [number, number, number, number] {\n const [r, g, b] = hexToRgb8(hex);\n return [r, g, b, 255];\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n return '#' + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('');\n}\n\nfunction unionRect(a: RasterRect | null, b: RasterRect | null): RasterRect | null {\n if (!a) return b ? { ...b } : null;\n if (!b) return { ...a };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\nconst isPixelLayer = (l: PaintLayer | undefined): l is PaintPixelLayer => l?.type === 'pixel';\n\nexport class PaintEditorController {\n private doc: PaintDocumentState;\n private extensions: PaintDocumentExtensions = {};\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private readonly historyLimit: number;\n private brush: PaintBrush;\n private userBrushes: PaintBrush[];\n /** The library list in the state (the same array until the user brushes change, so thumbnails are not redrawn). */\n private library: readonly PaintBrush[] | null = null;\n private color: string;\n private secondaryColor = '#ffffff';\n /** Gradient paint chosen with `setPaint` (`null`: the paint colour). */\n private gradient: PaintGradientSource | null = null;\n private mixerLoad: MixerLoad = EMPTY_MIXER_LOAD;\n private view: PaintView = { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };\n private hand: Hand;\n private readonly encodeImage: ImageEncoder | undefined;\n private listeners = new Set<() => void>();\n private version = 0;\n private cached: PaintEditorState | null = null;\n private session: PaintStrokeSession | null = null;\n private live: { layerId: string; surface: TiledSurface } | null = null;\n private liveLayers: { key: TiledSurface; layers: PaintLayer[] } | null = null;\n private dirty: RasterRect[] | 'all' = 'all';\n private seedCounter = 1;\n private strokeCounter = 1;\n private guides: PaintGuides;\n private swatches: Swatch[] = DEFAULT_SWATCHES.map((s) => ({ ...s }));\n private readonly pad = new MixingPad();\n private savedPads: SavedMixingPad[] = [];\n private nextPadId = 1;\n private tracingVersion = 1;\n private tracingCache: { key: number; node: ImageNode } | null = null;\n private readonly imagePixels = new Map<string, PixelImage>();\n private nextReferenceId = 1;\n private readonly viewCompositor = createPaintCompositor();\n private readonly exportCompositor = createPaintCompositor();\n /** Wet media: the simulation, its settings and time. */\n readonly wet: PaintWetLayers;\n private readonly wetOptions: PaintWetOptions | undefined;\n /** Where the light comes from for thick paint (saved with the document, not undo steps). */\n private lighting: PaintLighting = { ...DEFAULT_LIGHTING };\n private readonly litCaches = new Map<string, LitCache>();\n /** The live impasto stroke's channels. */\n private liveThick: { layerId: string; channels: ThickChannels } | null = null;\n /** Set while an operation that changes wet paint runs: the simulation before it (for undo). */\n private wetOp: { before: WetSnapshot | null } | null = null;\n\n constructor(options: PaintEditorOptions = {}) {\n this.historyLimit = Math.max(1, options.historyLimit ?? 200);\n this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });\n this.userBrushes = (options.brushes ?? []).map((b) => normalizePaintBrush({ ...b, builtin: false }));\n const start = this.findBrush(options.brushId ?? DEFAULT_BRUSH_ID) ?? this.findBrush(DEFAULT_BRUSH_ID)!;\n this.brush = normalizePaintBrush(start);\n this.color = options.color ? parseColor(options.color) : '#1f2937';\n this.hand = options.hand === 'left' ? 'left' : 'right';\n this.encodeImage = options.encodeImage;\n this.guides = defaultGuides(this.doc.canvas);\n this.wetOptions = options.wet;\n this.wet = new PaintWetLayers(\n {\n canvas: () => this.doc.canvas,\n paper: () => this.doc.paper,\n layerSurface: (id) => {\n const l = findLayer(this.doc.layers, id)?.layer;\n return isPixelLayer(l) ? l.surface : null;\n },\n updateSurfaces: (surfaces, rects) => {\n let layers = this.doc.layers;\n for (const [id, surface] of surfaces) if (findLayer(layers, id)) layers = mapLayer(layers, id, (l) => ({ ...(l as PaintPixelLayer), surface }));\n // Wet paint moving is not an undo step: keep the history's documents.\n this.doc = { ...this.doc, layers };\n this.emit(rects.length ? rects : 'all');\n },\n changed: () => {},\n },\n options.wet\n );\n this.wet.onDriverDone = () => this.session?.completeWetEnd();\n }\n\n // --- State and events ----------------------------------------------------------\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private emit(dirty: RasterRect[] | 'all' = 'all'): void {\n if (dirty === 'all' || this.dirty === 'all') this.dirty = 'all';\n else this.dirty.push(...dirty);\n this.version++;\n this.cached = null;\n for (const l of [...this.listeners]) l();\n }\n\n /** A snapshot of the editor (the same object until something changes). */\n getState(): PaintEditorState {\n if (this.cached) return this.cached;\n const last = this.undoStack[this.undoStack.length - 1];\n const next = this.redoStack[this.redoStack.length - 1];\n this.cached = {\n document: this.doc,\n brush: this.brush,\n brushes: (this.library ??= [...BUILTIN_BRUSHES, ...this.userBrushes]),\n color: this.color,\n secondaryColor: this.secondaryColor,\n paint: this.gradient ?? { type: 'color', color: this.color },\n mixerLoad: this.mixerLoad,\n view: this.view,\n hand: this.hand,\n canUndo: this.undoStack.length > 0 && !this.session,\n canRedo: this.redoStack.length > 0 && !this.session,\n undoLabel: last?.label ?? null,\n redoLabel: next?.label ?? null,\n guides: this.guides,\n swatches: this.swatches,\n mixingPadVersion: this.pad.version,\n savedMixingPads: this.savedPads.map(({ id, name }) => ({ id, name })),\n stroking: Boolean(this.session),\n wet: {\n layerIds: this.wet.wetLayerIds(),\n wet: this.wet.isWet(),\n paused: this.wet.paused,\n overlay: this.wet.overlay,\n instantDry: this.wet.instantDry,\n speed: this.wet.speed,\n clock: this.wet.world?.clock ?? 0,\n tilt: { ...this.wet.tilt },\n gpu: this.wet.gpuMode,\n gpuActive: Boolean(this.wet.world?.gpuActive),\n },\n version: this.version,\n };\n return this.cached;\n }\n\n get document(): PaintDocumentState {\n return this.doc;\n }\n\n /**\n * Where the rendered stack changed since the last call: rectangles (a stroke's dabs) or `'all'`.\n * The viewport passes it to `compositeTiles` as `dirty`.\n */\n takeDirty(): RasterRect[] | 'all' {\n const d = this.dirty;\n this.dirty = [];\n return d;\n }\n\n // --- History -------------------------------------------------------------------\n\n private requireIdle(what: string): void {\n if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);\n }\n\n private commit(label: string, next: PaintDocumentState, before = this.doc): void {\n const entry: HistoryEntry = { label, before, after: next };\n if (this.wetOp) {\n entry.wet = { before: this.wetOp.before, after: this.wet.snapshot(), lost: false };\n this.wetOp = null;\n }\n this.undoStack.push(entry);\n if (this.undoStack.length > this.historyLimit) this.undoStack.shift();\n this.redoStack = [];\n this.doc = next;\n this.trimWetHistory();\n this.emit();\n }\n\n /** Keeps the wet snapshots in history within the wet undo memory, dropping the oldest. */\n private trimWetHistory(): void {\n const seen = new Set<WetSnapshot>();\n let bytes = 0;\n // The nearest steps first: the top of the undo stack, then the redo stack.\n const order = [...[...this.undoStack].reverse(), ...[...this.redoStack].reverse()];\n for (const e of order) {\n if (!e.wet || e.wet.lost) continue;\n for (const snap of [e.wet.before, e.wet.after]) if (snap && !seen.has(snap)) {\n seen.add(snap);\n bytes += snap.bytes;\n }\n if (bytes > this.wet.undoMemory) e.wet = { before: null, after: null, lost: true };\n }\n }\n\n /** Throws when wet paint is mid-step (a batch in worker threads); `wetIdle()` waits for it. */\n private requireWetIdle(what: string): void {\n if (this.wet.busy) throw new PaintEditorError(`Wet paint is mid-step; wait a moment (await wetIdle()) before ${what}.`);\n }\n\n /** Starts an operation that changes wet paint: keeps the simulation as it is now for undo. */\n private beginWetOp(): void {\n this.requireWetIdle('changing wet paint');\n if (!this.wetOp) this.wetOp = { before: this.wet.snapshot() };\n }\n\n /**\n * Before a non-wet change to layers that hold wet paint (a dry brush, a fill, a merge…): dries\n * them first, in the same undo step. Returns the ids dried.\n */\n private dryLayersFirst(ids: readonly string[]): string[] {\n const wetIds = ids.filter((id) => this.wet.wetLayerIds().includes(id));\n if (!wetIds.length) return [];\n this.beginWetOp();\n for (const id of wetIds) this.wet.dry(id);\n this.logWet({ kind: 'dry', layerId: wetIds.join(',') });\n return wetIds;\n }\n\n /** Records a wet event (for replays) in the document being built. */\n private pendingWetEvents: WetEvent[] = [];\n\n private logWet(e: Omit<WetEvent, 'clock'>): void {\n this.pendingWetEvents.push({ clock: this.wet.world?.clock ?? 0, at: this.doc.strokes.length, ...e });\n }\n\n /** The document with a paint event appended (for replays). */\n private withPaintEvent(doc: PaintDocumentState, e: Omit<PaintEvent, 'at'>): PaintDocumentState {\n return { ...doc, paintEvents: [...(doc.paintEvents ?? []), { at: doc.strokes.length, ...e }] };\n }\n\n /** The document with the pending wet events appended. */\n private withWetEvents(doc: PaintDocumentState): PaintDocumentState {\n if (!this.pendingWetEvents.length) return doc;\n const events = [...(doc.wetEvents ?? []), ...this.pendingWetEvents];\n this.pendingWetEvents = [];\n return { ...doc, wetEvents: events };\n }\n\n undo(): string | null {\n this.requireIdle('undoing');\n const e = this.undoStack[this.undoStack.length - 1];\n if (!e) return null;\n if (e.wet) this.requireWetIdle('undoing');\n this.undoStack.pop();\n this.redoStack.push(e);\n const paper = this.doc.paper;\n this.doc = e.before;\n if (e.wet) {\n if (paper !== this.doc.paper) this.wet.paperChanged();\n this.wet.restore(e.wet.lost ? null : e.wet.before);\n if (paper !== this.doc.paper) this.wet.paperChanged();\n }\n this.emit();\n return e.label;\n }\n\n redo(): string | null {\n this.requireIdle('redoing');\n const e = this.redoStack[this.redoStack.length - 1];\n if (!e) return null;\n if (e.wet) this.requireWetIdle('redoing');\n this.redoStack.pop();\n this.undoStack.push(e);\n const paper = this.doc.paper;\n this.doc = e.after;\n if (e.wet) {\n this.wet.restore(e.wet.lost ? null : e.wet.after);\n if (paper !== this.doc.paper) this.wet.paperChanged();\n }\n this.emit();\n return e.label;\n }\n\n /** Labels of the undo steps, oldest first. */\n historyLabels(): { undo: string[]; redo: string[] } {\n return { undo: this.undoStack.map((e) => e.label), redo: this.redoStack.map((e) => e.label).reverse() };\n }\n\n // --- Canvas and paper ----------------------------------------------------------\n\n private resolvePaper(paper: Partial<PaperSettings> | string | undefined): Partial<PaperSettings> {\n if (typeof paper === 'string') {\n const p = paperPreset(paper);\n if (!p) throw new PaintEditorError(`Unknown paper \"${paper}\". Call list_papers for the ids.`);\n return p;\n }\n return paper ?? {};\n }\n\n /** Starts a new canvas: one empty layer over the paper (one undo step; undo brings the old one back). */\n newCanvas(options: Partial<PaintCanvas> & { paper?: Partial<PaperSettings> | string } = {}): PaintDocumentState {\n this.requireIdle('starting a new canvas');\n const next = createPaintDocument({\n width: options.width ?? this.doc.canvas.width,\n height: options.height ?? this.doc.canvas.height,\n dpi: options.dpi ?? this.doc.canvas.dpi,\n paper: options.paper === undefined ? this.doc.paper : this.resolvePaper(options.paper),\n });\n this.extensions = {};\n this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };\n this.tracingVersion++;\n if (this.wet.world) {\n this.beginWetOp();\n this.wet.reset();\n }\n this.commit('New canvas', next);\n return next;\n }\n\n /** Changes the canvas size (keeping the pixels where they are, anchored top-left) or resolution. */\n setCanvas(update: Partial<PaintCanvas>): PaintCanvas {\n this.requireIdle('changing the canvas');\n const canvas = normalizeCanvas({\n width: update.width ?? this.doc.canvas.width,\n height: update.height ?? this.doc.canvas.height,\n dpi: update.dpi ?? this.doc.canvas.dpi,\n });\n if (this.wet.world && (canvas.width !== this.doc.canvas.width || canvas.height !== this.doc.canvas.height)) {\n // The wet simulation is canvas-sized: the paint dries first.\n this.dryLayersFirst(this.wet.wetLayerIds());\n this.wet.reset();\n }\n this.commit('Canvas size', this.withWetEvents({ ...this.doc, canvas }));\n return canvas;\n }\n\n /** Changes the paper: a library preset (`preset`), and / or any of its settings (one undo step). */\n setPaper(update: Partial<PaperSettings> & { preset?: string }): PaperSettings {\n this.requireIdle('changing the paper');\n let base = this.doc.paper;\n if (update.preset !== undefined && update.preset !== this.doc.paper.preset) {\n const p = paperPreset(update.preset);\n if (!p) throw new PaintEditorError(`Unknown paper \"${update.preset}\". Call list_papers for the ids.`);\n base = p;\n }\n const { preset: _p, ...rest } = update;\n if (rest.color !== undefined) parseColor(rest.color, 'paper color');\n const paper = normalizePaper({ ...rest, preset: base.preset }, base);\n const changedKeys = Object.keys(rest).filter((k) => k !== 'name');\n if (changedKeys.length && update.preset === undefined) paper.preset = base.preset;\n if (this.wet.world) this.beginWetOp();\n this.commit('Paper', { ...this.doc, paper });\n this.wet.paperChanged();\n return clonePaper(paper);\n }\n\n // --- Layers --------------------------------------------------------------------\n\n getLayer(id: string): PaintLayer | undefined {\n return findLayer(this.doc.layers, id)?.layer;\n }\n\n /** The active pixel layer (or `undefined`). */\n getActiveLayer(): PaintLayer | undefined {\n return this.doc.activeLayerId ? this.getLayer(this.doc.activeLayerId) : undefined;\n }\n\n /** Every layer, top first, as panels and tools describe them. */\n listLayers(): PaintLayerInfo[] {\n const out: PaintLayerInfo[] = [];\n const walk = (list: readonly PaintLayer[], parentId: string | null, depth: number) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const l = list[i];\n out.push(this.describeLayer(l, parentId, depth));\n if (l.type === 'group') walk(l.children, l.id, depth + 1);\n }\n };\n walk(this.doc.layers, null, 0);\n return out;\n }\n\n describeLayer(l: PaintLayer, parentId: string | null = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0): PaintLayerInfo {\n let bounds: RasterRect | null = null;\n if (l.type === 'pixel') {\n const b = contentBounds(l.surface);\n bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;\n } else for (const c of flattenLayers(l.children)) if (c.type === 'pixel') bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);\n return {\n id: l.id,\n name: l.name,\n type: l.type,\n visible: l.visible,\n opacity: l.opacity,\n blend: l.blend,\n clipped: Boolean(l.clipped),\n locks: { ...(l.locks ?? {}) },\n active: l.id === this.doc.activeLayerId,\n bounds,\n parentId,\n depth,\n };\n }\n\n private requireLayer(id: string): PaintLayer {\n const l = this.getLayer(id);\n if (!l) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n return l;\n }\n\n /** The pixel layer a paint operation targets, checked for locks and visibility. */\n private paintTarget(layerId: string | undefined, what: string): PaintPixelLayer {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) throw new PaintEditorError(`There is no active layer to ${what}; add one with add_layer.`);\n const l = this.requireLayer(id);\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; ${what} a pixel layer inside it.`);\n if (isLayerLocked(l, 'pixels')) throw new PaintEditorError(`Layer \"${l.name}\" has its pixels locked; unlock it (update_layer locks) before you ${what} it.`);\n if (!l.visible) throw new PaintEditorError(`Layer \"${l.name}\" is hidden; show it (update_layer visible true) before you ${what} it.`);\n return l;\n }\n\n setActiveLayer(id: string): void {\n this.requireLayer(id);\n if (this.doc.activeLayerId === id) return;\n // Selection of the active layer is editor state, not an undo step: keep the history's documents.\n this.doc = { ...this.doc, activeLayerId: id };\n this.emit();\n }\n\n /** Adds an empty pixel layer or group above `aboveId` (default: the active layer) and makes it active. */\n addLayer(options: { name?: string; type?: 'pixel' | 'group'; aboveId?: string; parentId?: string | null } = {}): PaintLayerInfo {\n this.requireIdle('adding a layer');\n const count = flattenLayers(this.doc.layers).length;\n const type = options.type ?? 'pixel';\n const name = options.name?.trim() || (type === 'group' ? `Group ${count + 1}` : `Layer ${count + 1}`);\n const layer: PaintLayer =\n type === 'group' ? { type: 'group', id: newPaintLayerId(), name, visible: true, opacity: 1, blend: 'pass-through', children: [] } : createPaintLayer(name);\n let parentId: string | null = null;\n let index: number | undefined;\n const above = options.aboveId ?? (options.parentId === undefined ? this.doc.activeLayerId : null);\n if (options.parentId !== undefined && options.parentId !== null) {\n const g = this.requireLayer(options.parentId);\n if (g.type !== 'group') throw new PaintEditorError(`Layer \"${g.name}\" is not a group; pass a group id as parentId.`);\n parentId = g.id;\n } else if (above) {\n const loc = findLayer(this.doc.layers, above);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${above}\". Call list_layers for the ids.`);\n parentId = loc.parent?.id ?? null;\n index = loc.index + 1;\n }\n const layers = insertLayer(this.doc.layers, layer, parentId, index);\n this.commit(type === 'group' ? 'Add group' : 'Add layer', { ...this.doc, layers, activeLayerId: layer.id });\n return this.describeLayer(layer);\n }\n\n updateLayer(id: string, update: LayerUpdate): PaintLayerInfo {\n this.requireIdle('changing a layer');\n const l = this.requireLayer(id);\n if (isLayerLocked(l, 'all') && Object.keys(update).some((k) => k !== 'locks')) throw new PaintEditorError(`Layer \"${l.name}\" is fully locked; unlock it first (locks {all: false}).`);\n if (update.opacity !== undefined && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);\n if (update.blend !== undefined && !isBlendMode(update.blend) && !(l.type === 'group' && update.blend === 'pass-through')) {\n throw new PaintEditorError(`Unknown blend mode \"${update.blend}\". Call get_canvas_info for the blend modes.`);\n }\n if (update.name !== undefined && !update.name.trim()) throw new PaintEditorError('name is empty; give the layer a name.');\n const next = { ...l } as PaintLayer;\n if (update.name !== undefined) next.name = update.name.trim();\n if (update.visible !== undefined) next.visible = update.visible;\n if (update.opacity !== undefined) next.opacity = update.opacity;\n if (update.blend !== undefined) (next as { blend: string }).blend = update.blend;\n if (update.clipped !== undefined) next.clipped = update.clipped || undefined;\n if (update.locks !== undefined) {\n const locks = { ...(l.locks ?? {}), ...update.locks };\n next.locks = Object.values(locks).some(Boolean) ? locks : undefined;\n }\n this.commit('Layer properties', { ...this.doc, layers: mapLayer(this.doc.layers, id, () => next) });\n return this.describeLayer(next);\n }\n\n removeLayer(id: string): { removed: string; activeLayerId: string | null } {\n this.requireIdle('removing a layer');\n const l = this.requireLayer(id);\n if (isLayerLocked(l, 'all')) throw new PaintEditorError(`Layer \"${l.name}\" is fully locked; unlock it before removing it.`);\n const wetIds = this.wet.wetLayerIds().filter((x) => x === id || (l.type === 'group' && findLayer(l.children, x)));\n if (wetIds.length) {\n this.beginWetOp();\n for (const x of wetIds) this.wet.world!.removeLane(x);\n }\n const layers = mapLayer(this.doc.layers, id, () => null);\n let active = this.doc.activeLayerId;\n if (active && !findLayer(layers, active)) active = flattenLayers(layers).filter((x) => x.type === 'pixel').pop()?.id ?? null;\n this.commit('Remove layer', { ...this.doc, layers, activeLayerId: active, ...this.thickWithout(flattenLayers([l]).map((x) => x.id)), ...this.stickersWithout(flattenLayers([l]).map((x) => x.id)) });\n return { removed: id, activeLayerId: active };\n }\n\n duplicateLayer(id: string): PaintLayerInfo {\n this.requireIdle('duplicating a layer');\n if (findLayer(this.doc.layers, id)) {\n const inside = (l: PaintLayer): string[] => (l.type === 'group' ? l.children.flatMap(inside) : [l.id]);\n this.dryLayersFirst(inside(findLayer(this.doc.layers, id)!.layer));\n }\n const loc = findLayer(this.doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const thick: Record<string, ThickChannels> = { ...(this.doc.thick ?? {}) };\n const stickers: Record<string, Sticker[]> = { ...(this.doc.stickers ?? {}) };\n const copy = (l: PaintLayer): PaintLayer => {\n const id = newPaintLayerId();\n if (thick[l.id]) thick[id] = thick[l.id];\n if (stickers[l.id]) stickers[id] = stickers[l.id].map((x) => ({ ...x }));\n return l.type === 'group' ? { ...l, id, children: l.children.map(copy) } : { ...l, id };\n };\n const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };\n const layers = insertLayer(this.doc.layers, dup, loc.parent?.id ?? null, loc.index + 1);\n this.commit('Duplicate layer', this.withWetEvents({ ...this.doc, layers, activeLayerId: dup.id, ...(this.doc.thick ? { thick } : {}), ...(this.doc.stickers ? { stickers } : {}) }));\n return this.describeLayer(dup);\n }\n\n /** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */\n moveLayer(id: string, index: number, parentId?: string | null): PaintLayerInfo {\n this.requireIdle('moving a layer');\n const loc = findLayer(this.doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const target = parentId === undefined ? (loc.parent?.id ?? null) : parentId;\n if (target) {\n const g = this.requireLayer(target);\n if (g.type !== 'group') throw new PaintEditorError(`Layer \"${g.name}\" is not a group; pass a group id as parentId.`);\n if (target === id || (loc.layer.type === 'group' && findLayer(loc.layer.children, target))) throw new PaintEditorError('A group cannot move into itself.');\n }\n const without = mapLayer(this.doc.layers, id, () => null);\n const siblings = target ? (findLayer(without, target)!.layer as { children: PaintLayer[] }).children : without;\n if (!Number.isInteger(index) || index < 0 || index > siblings.length) throw new PaintEditorError(`index ${index} is out of range; use 0..${siblings.length} (0 is the bottom).`);\n const layers = insertLayer(without, loc.layer, target, index);\n this.commit('Move layer', { ...this.doc, layers });\n return this.describeLayer(loc.layer);\n }\n\n /** Merges a pixel layer into the pixel layer below it (keeping the lower one's settings). */\n mergeDown(id: string): PaintLayerInfo {\n this.requireIdle('merging');\n const pre = findLayer(this.doc.layers, id);\n if (pre) {\n const sib = pre.parent ? pre.parent.children : this.doc.layers;\n this.dryLayersFirst([id, sib[pre.index - 1]?.id].filter((x): x is string => Boolean(x)));\n }\n const loc = findLayer(this.doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const siblings = loc.parent ? loc.parent.children : this.doc.layers;\n const below = siblings[loc.index - 1];\n const upper = loc.layer;\n if (!isPixelLayer(upper) || !isPixelLayer(below)) throw new PaintEditorError('Merge down needs a pixel layer directly above another pixel layer in the same group.');\n if (isLayerLocked(below, 'pixels')) throw new PaintEditorError(`Layer \"${below.name}\" has its pixels locked; unlock it to merge into it.`);\n const rect = unionRect(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);\n let surface = emptySurface(4);\n if (rect) {\n const base: PaintPixelLayer = { ...below, opacity: 1, blend: 'normal', visible: true, clipped: undefined, mask: undefined };\n // Thick paint on the upper layer merges as it is lit (its relief does not carry over).\n const upperLit = this.litLayers([upper])[0] as PaintPixelLayer;\n const px = createPaintCompositor().composite([base, upper.visible ? upperLit : { ...upperLit, opacity: 0 }] as ViewLayer[], rect, this.sources());\n surface = surfaceFromPixels({ width: rect.width, height: rect.height, data: px }, 4, rect.x, rect.y);\n }\n const merged: PaintPixelLayer = { ...below, surface, offset: { x: 0, y: 0 } };\n const layers = mapLayer(mapLayer(this.doc.layers, upper.id, () => null), below.id, () => merged);\n this.commit('Merge down', this.withWetEvents({ ...this.doc, layers, activeLayerId: below.id, ...this.thickWithout([upper.id]), ...this.stickersWithout([upper.id, below.id]) }));\n return this.describeLayer(merged);\n }\n\n /** Removes every pixel of a layer. */\n clearLayer(layerId?: string): { layerId: string; bounds: RasterRect | null } {\n this.requireIdle('clearing');\n const l = this.paintTarget(layerId, 'clear');\n if (this.wet.wetLayerIds().includes(l.id)) {\n this.beginWetOp();\n this.wet.world!.removeLane(l.id);\n }\n const bounds = this.describeLayer(l).bounds;\n this.commit('Clear layer', this.withPaintEvent({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })), ...this.thickWithout([l.id]), ...this.stickersWithout([l.id]) }, { kind: 'clear', layerId: l.id }));\n return { layerId: l.id, bounds };\n }\n\n /**\n * Fills a layer across the canvas with a colour. With `grain` above 0 the fill follows the paper\n * grain (it catches the peaks, as pastel or charcoal rubbed across the sheet).\n */\n fillLayer(options: FillLayerOptions = {}): { layerId: string; bounds: RasterRect } {\n this.requireIdle('filling');\n const fillTarget = this.paintTarget(options.layerId, 'fill');\n if (this.doc.stickers?.[fillTarget.id]?.length) throw new PaintEditorError(`Layer \"${fillTarget.name}\" is a sticker layer; fill another layer (add_layer).`);\n this.dryLayersFirst([options.layerId ?? this.doc.activeLayerId ?? '']);\n const l = this.paintTarget(options.layerId, 'fill');\n const color = options.color === undefined ? this.color : parseColor(options.color);\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new PaintEditorError(`opacity ${opacity} is out of range; use 0..1.`);\n const grainAmount = options.grain ?? 0;\n if (!(grainAmount >= 0 && grainAmount <= 1)) throw new PaintEditorError(`grain ${grainAmount} is out of range; use 0..1.`);\n const pressure = options.pressure ?? 0.5;\n const blend = options.blend ?? 'normal';\n if (!isBlendMode(blend)) throw new PaintEditorError(`Unknown blend mode \"${blend}\".`);\n const { width, height } = this.doc.canvas;\n const rect = { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const depth = grainAmount * this.doc.paper.depth;\n const grain = depth > 0 ? paperGrain(this.doc.paper) : null;\n for (const { tx, ty } of tilesInRect(rect)) {\n const r = { x: tx * TILE, y: ty * TILE, width: Math.min(TILE, width - tx * TILE), height: Math.min(TILE, height - ty * TILE) };\n const vals = new Float32Array(r.width * r.height);\n for (let y = 0, k = 0; y < r.height; y++) for (let x = 0; x < r.width; x++, k++) vals[k] = grain ? grainCatch(grain.at(r.x + x + 0.5, r.y + y + 0.5), depth, pressure) : 1;\n coverage.accumulate(r, vals, 1);\n }\n const apply: StrokeApply = {\n mode: 'paint',\n color: rgbaOf(color),\n background: rgbaOf(this.doc.paper.color),\n opacity,\n blend,\n lockTransparency: isLayerLocked(l, 'transparency'),\n selection: null,\n offset: l.offset,\n };\n const surface = this.applyCoverage(l, coverage, rect, apply);\n this.commit('Fill layer', this.withPaintEvent(this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })) }), { kind: 'fill', layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }));\n return { layerId: l.id, bounds: rect };\n }\n\n private applyCoverage(l: PaintPixelLayer, coverage: CoverageMap, rect: RasterRect, apply: StrokeApply): TiledSurface {\n const base = l.surface;\n const tiles = new Map(base.tiles);\n for (const { tx, ty } of tilesInRect({ x: rect.x - l.offset.x, y: rect.y - l.offset.y, width: rect.width, height: rect.height })) {\n const key = tileKey(tx, ty);\n const src = base.tiles.get(key)?.data ?? new Uint8ClampedArray(TILE * TILE * 4);\n const tile = finishTile(applyStrokeToTile(coverage, src, 4, tx, ty, apply), 4, 0);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n return new TiledSurface(4, tiles, 0);\n }\n\n // --- Brushes -------------------------------------------------------------------\n\n private findBrush(id: string): PaintBrush | undefined {\n return BUILTIN_BRUSHES.find((b) => b.id === id) ?? this.userBrushes.find((b) => b.id === id);\n }\n\n /** A library brush by id (`undefined` when unknown). */\n getBrush(id: string): PaintBrush | undefined {\n const b = this.findBrush(id);\n return b ? normalizePaintBrush(b, b) : undefined;\n }\n\n private requireBrush(id: string): PaintBrush {\n const b = this.findBrush(id);\n if (!b) throw new PaintEditorError(`Unknown brush \"${id}\". Call list_brushes for the ids.`);\n return b;\n }\n\n /**\n * Chooses the active brush (`id`: a library brush) and / or changes its settings. Editor state,\n * not an undo step. Returns the active brush.\n */\n setBrush(update: { id?: string } & Omit<PaintBrushInput, 'id'> = {}): PaintBrush {\n const { id, ...changes } = update;\n const base = id !== undefined ? this.requireBrush(id) : this.brush;\n // Gradient paint is checked strictly here (documents and presets are read leniently).\n if (changes.paint !== undefined && changes.paint !== null) this.readStrokePaint(changes.paint);\n const next = normalizePaintBrush({ ...changes, id: base.id }, base);\n // The active brush is `builtin` only while it is an unchanged built-in preset.\n if (base.builtin && !Object.keys(changes).length) next.builtin = true;\n else delete next.builtin;\n this.brush = next;\n this.emit([]);\n return this.brush;\n }\n\n /** Saves the active brush (or `brush`) into the user library under `id` / `name`. */\n saveBrush(options: { id: string; name?: string; brush?: PaintBrushInput }): PaintBrush {\n if (!BRUSH_ID.test(options.id)) throw new PaintEditorError(`Brush id \"${options.id}\" is invalid; use lowercase letters, digits and dashes (e.g. \"my-pencil\").`);\n if (BUILTIN_BRUSHES.some((b) => b.id === options.id)) throw new PaintEditorError(`\"${options.id}\" is a built-in brush; save under another id.`);\n const source = options.brush ? normalizePaintBrush(options.brush, this.brush) : this.brush;\n const saved = normalizePaintBrush({ ...source, id: options.id, name: options.name ?? source.name });\n delete saved.builtin;\n const i = this.userBrushes.findIndex((b) => b.id === options.id);\n if (i >= 0) this.userBrushes[i] = saved;\n else this.userBrushes.push(saved);\n this.library = null;\n this.emit([]);\n return saved;\n }\n\n removeBrush(id: string): void {\n if (BUILTIN_BRUSHES.some((b) => b.id === id)) throw new PaintEditorError(`\"${id}\" is a built-in brush and cannot be removed.`);\n const i = this.userBrushes.findIndex((b) => b.id === id);\n if (i < 0) throw new PaintEditorError(`Unknown brush \"${id}\". Call list_brushes for the ids.`);\n this.userBrushes.splice(i, 1);\n this.library = null;\n this.emit([]);\n }\n\n /** The user's saved brushes. */\n getUserBrushes(): PaintBrush[] {\n return this.userBrushes.map((b) => normalizePaintBrush(b, b));\n }\n\n /** A brush library file of the given brushes (default: the user's brushes). */\n exportBrushes(ids?: readonly string[]): BrushLibraryFile {\n const list = ids ? ids.map((id) => this.requireBrush(id)) : this.userBrushes;\n return { format: 'pixerate-brushes', version: 1, brushes: list.map(brushToJSON) };\n }\n\n /**\n * Adds the brushes of a library file to the user library. A brush whose id is taken is skipped\n * unless `replace` (built-in ids are always skipped). Returns the ids added and the problems.\n */\n importBrushes(file: unknown, options: { replace?: boolean } = {}): { added: string[]; skipped: string[]; problems: string[] } {\n const problems: string[] = [];\n let list: PaintBrush[];\n try {\n list = parseBrushLibrary(file, problems);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n const added: string[] = [];\n const skipped: string[] = [];\n for (const b of list) {\n if (BUILTIN_BRUSHES.some((x) => x.id === b.id)) {\n skipped.push(b.id);\n problems.push(`\"${b.id}\" is a built-in brush id; rename it to import it.`);\n continue;\n }\n const i = this.userBrushes.findIndex((x) => x.id === b.id);\n if (i >= 0 && !options.replace) {\n skipped.push(b.id);\n continue;\n }\n if (i >= 0) this.userBrushes[i] = b;\n else this.userBrushes.push(b);\n added.push(b.id);\n }\n if (added.length) {\n this.library = null;\n this.emit([]);\n }\n return { added, skipped, problems };\n }\n\n /** A thumbnail of a brush: a test stroke on the current paper. */\n brushThumbnail(id: string, options: { width?: number; height?: number; color?: string } = {}): BrushThumbnail {\n const b = id === this.brush.id ? this.brush : this.requireBrush(id);\n return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });\n }\n\n // --- Colour --------------------------------------------------------------------\n\n /** Sets the paint colour (and goes back to one colour from gradient paint). */\n setColor(color: string): string {\n this.color = parseColor(color);\n this.gradient = null;\n this.emit([]);\n return this.color;\n }\n\n /**\n * Sets what strokes paint with: one colour (`{ type: 'color', color }` or `#rrggbb`), or gradient\n * paint (`{ type: 'gradient', stops, space?, map?, softness? }`, see `@pixerate/editor/paint`),\n * which brushes sample dab by dab as the stroke goes. Not an undo step. Throws with what is wrong\n * with an invalid source. Returns the paint.\n */\n setPaint(paint: PaintSource | string): PaintSource {\n let p: PaintSource;\n try {\n p = parsePaintSource(paint);\n } catch (e) {\n if (e instanceof PaintSourceError) throw new PaintEditorError(e.message);\n throw e;\n }\n if (p.type === 'color') {\n this.setColor(p.color);\n return p;\n }\n this.gradient = p;\n this.emit([]);\n return p;\n }\n\n /**\n * The gradient a stroke paints (resolved against its colours: no `paint` / `secondary` stops),\n * or `null` for one colour: the stroke's own `paint`, else (without an explicit `color`) the\n * brush's gradient, else the editor's gradient paint. Erasers, blenders and smudges take none.\n */\n private strokeGradient(init: PaintStrokeInit, brush: PaintBrush, color: string): PaintGradientSource | null {\n if (!brushTakesPaint(brush)) return null;\n let g: PaintGradientSource | null = null;\n if (init.paint !== undefined) {\n const p = this.readStrokePaint(init.paint);\n g = p.type === 'gradient' ? p : null;\n } else g = brush.paint ?? (init.color === undefined ? this.gradient : null);\n return g ? resolvePaintGradient(g, { paint: color, secondary: this.secondaryColor }) : null;\n }\n\n private readStrokePaint(raw: unknown): PaintSource {\n try {\n return parsePaintSource(raw);\n } catch (e) {\n if (e instanceof PaintSourceError) throw new PaintEditorError(e.message);\n throw e;\n }\n }\n\n setSecondaryColor(color: string): string {\n this.secondaryColor = parseColor(color);\n this.emit([]);\n return this.secondaryColor;\n }\n\n swapColors(): void {\n [this.color, this.secondaryColor] = [this.secondaryColor, this.color];\n this.emit([]);\n }\n\n /** The colour at a document pixel: of the composite (paper included) or of one layer. */\n pickColor(x: number, y: number, options: { layerId?: string } = {}): { color: string; alpha: number } {\n const { width, height } = this.doc.canvas;\n const px = Math.floor(x);\n const py = Math.floor(y);\n if (!(px >= 0 && py >= 0 && px < width && py < height)) throw new PaintEditorError(`(${x}, ${y}) is outside the ${width}×${height} canvas.`);\n if (options.layerId !== undefined) {\n const l = this.requireLayer(options.layerId);\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; pick from a pixel layer or the composite.`);\n const [r, g, b, a] = readPixel(l.surface, px - l.offset.x, py - l.offset.y);\n return { color: rgbToHex(r, g, b), alpha: Math.round((a / 255) * 1000) / 1000 };\n }\n const d = this.exportCompositor.composite(this.renderLayers({}), { x: px, y: py, width: 1, height: 1 }, this.sources());\n return { color: rgbToHex(d[0], d[1], d[2]), alpha: Math.round((d[3] / 255) * 1000) / 1000 };\n }\n\n // --- Mixer Brush ---------------------------------------------------------------\n\n /** Fills the Mixer Brush reservoir with a colour (default the editor colour). */\n loadMixerBrush(color?: string): MixerLoad {\n const [r, g, b] = hexToRgb8(color === undefined ? this.color : parseColor(color));\n this.mixerLoad = loadMixer(this.mixerLoad, [r / 255, g / 255, b / 255]);\n this.emit([]);\n return this.mixerLoad;\n }\n\n /** Empties the pickup well (and with `emptyReservoir` the loaded paint too). */\n cleanMixerBrush(emptyReservoir = false): MixerLoad {\n this.mixerLoad = emptyReservoir ? EMPTY_MIXER_LOAD : cleanMixer(this.mixerLoad);\n this.emit([]);\n return this.mixerLoad;\n }\n\n // --- View and hand -------------------------------------------------------------\n\n /** Zoom, pan, rotation and mirroring: view state only, the pixels never resample. */\n setView(update: Partial<PaintView>): PaintView {\n this.view = normalizeView(update, this.view);\n this.emit([]);\n return this.view;\n }\n\n resetView(): PaintView {\n return this.setView({ ...DEFAULT_VIEW, pan: { x: 0, y: 0 } });\n }\n\n setHand(hand: Hand): Hand {\n if (hand !== 'left' && hand !== 'right') throw new PaintEditorError(`hand is ${JSON.stringify(hand)}; use \"left\" or \"right\".`);\n this.hand = hand;\n this.emit([]);\n return hand;\n }\n\n // --- Strokes -------------------------------------------------------------------\n\n private strokeBrush(init: PaintStrokeInit): PaintBrush {\n let brush = init.brushId !== undefined ? this.requireBrush(init.brushId) : this.brush;\n if (init.brush) brush = normalizePaintBrush({ ...init.brush, id: brush.id }, brush);\n return brush;\n }\n\n /**\n * After a stroke: a Mixer Brush that loads colour keeps its whole load; any other dirty brush\n * keeps only the paint it carries (the reservoir stays the Mixer Brush's).\n */\n private keepLoad(brush: PaintBrush, load: MixerLoad): void {\n if (brush.engine === 'mixer' && brush.loadsColor) this.mixerLoad = load;\n else if (brush.behavior.dirty) this.mixerLoad = { ...this.mixerLoad, pickup: load.pickup };\n }\n\n /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */\n protected strokeCopies(): Affine[] {\n return symmetryCopies(this.guides.symmetry);\n }\n\n private sessionFor(doc: PaintDocumentState, init: PaintStrokeInit, host: StrokeSessionHost): PaintStrokeSession {\n if (init.paint !== undefined) {\n const p = this.readStrokePaint(init.paint);\n if (p.type === 'color') init = { ...init, color: p.color, paint: undefined };\n else init = { ...init, paint: p };\n }\n let layer = (() => {\n const id = init.layerId ?? doc.activeLayerId;\n if (!id) throw new PaintEditorError('There is no active layer to paint on; add one with add_layer.');\n const loc = findLayer(doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const l = loc.layer;\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; paint on a pixel layer inside it.`);\n if (isLayerLocked(l, 'pixels')) throw new PaintEditorError(`Layer \"${l.name}\" has its pixels locked; unlock it (update_layer locks) to paint on it.`);\n if (!l.visible) throw new PaintEditorError(`Layer \"${l.name}\" is hidden; show it (update_layer visible true) to paint on it.`);\n return l;\n })();\n const brush = this.strokeBrush(init);\n const stickerLayer = Boolean(doc.stickers?.[layer.id]?.length);\n if (brush.engine === 'sticker') {\n if (!stickerLayer && contentBounds(layer.surface)) throw new PaintEditorError(`Layer \"${layer.name}\" already has paint; spray stickers onto an empty layer (add_layer) so they stay editable.`);\n } else if (stickerLayer) {\n throw new PaintEditorError(`Layer \"${layer.name}\" is a sticker layer (its pixels are drawn from its stickers); paint on another layer (add_layer).`);\n }\n let wet: { driver: WetStrokeDriver; surface: () => TiledSurface } | undefined;\n if (isWetBrush(brush)) {\n this.requireWetIdle('painting');\n if (layer.offset.x || layer.offset.y) {\n // The wet simulation works in canvas space: move the pixels into it (same picture, not an undo step).\n const moved: PaintPixelLayer = { ...layer, surface: translateSurface(layer.surface, layer.offset.x, layer.offset.y), offset: { x: 0, y: 0 } };\n this.doc = { ...this.doc, layers: mapLayer(this.doc.layers, layer.id, () => moved) };\n if (doc !== this.doc) doc = { ...doc, layers: mapLayer(doc.layers, layer.id, () => moved) };\n layer = moved;\n }\n if (!this.wet.wetLayerIds().includes(layer.id) && this.wet.wetLayerIds().length >= MAX_WET_LANES) {\n throw new PaintEditorError(`At most ${MAX_WET_LANES} layers can hold wet paint at once; dry one first (dry_paint).`);\n }\n this.beginWetOp();\n const world = this.wet.ensureLane(layer.id, brush.wet.medium);\n const id = layer.id;\n const wetColor = init.color === undefined ? this.color : parseColor(init.color);\n const [r, g, b] = hexToRgb8(wetColor);\n const wetGradient = this.strokeGradient(init, brush, wetColor);\n const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);\n const seedW = init.seed ?? mix32((this.seedCounter * 0x9e3779b1) >>> 0) & 0x7fffffff;\n const copiesW = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));\n const driver = new WetStrokeDriver({\n world,\n laneId: id,\n brush,\n color: [r / 255, g / 255, b / 255],\n load,\n grain: paperGrain(doc.paper),\n paperDepth: doc.paper.depth,\n paperColor: hexToRgb8(doc.paper.color),\n seed: seedW,\n copies: [IDENTITY_AFFINE, ...copiesW].map(affineCopy),\n mask: init.maskStencils ? this.stencilSelection(init.maskStencils) : init.stencils !== false ? this.stencilSelection() : null,\n paint: wetGradient,\n });\n init = { ...init, seed: seedW, load };\n wet = { driver, surface: () => world.display(id).surface };\n } else if (this.wet.wetLayerIds().includes(layer.id)) {\n // A dry brush on wet paint: the paint dries first (in the same undo step).\n this.dryLayersFirst([layer.id]);\n const dried = findLayer(this.doc.layers, layer.id)!.layer as PaintPixelLayer;\n layer = dried;\n }\n let color = init.color === undefined ? this.color : parseColor(init.color);\n const tracing = this.guides.tracing;\n if (init.color === undefined && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;\n const perspective = this.guides.perspective;\n const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;\n const useStencils = init.stencils !== false;\n const stencilSnapper = useStencils && this.guides.stencils.some((x) => x.enabled && x.mode === 'guide') ? new StencilSnapper(this.guides.stencils) : null;\n const selection = init.maskStencils ? this.stencilSelection(init.maskStencils) : useStencils ? this.stencilSelection() : null;\n const inputFilter = snapper || stencilSnapper ? (p: PenPoint) => (stencilSnapper ? stencilSnapper.snap(snapper ? snapper.snap(p) : p) : snapper!.snap(p)) : undefined;\n const rgba = rgbaOf(color);\n const seed = init.seed ?? mix32((this.seedCounter++ * 0x9e3779b1) >>> 0) & 0x7fffffff;\n const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));\n const session = new PaintStrokeSession(host, {\n layerId: layer.id,\n surface: layer.surface,\n offset: layer.offset,\n lockTransparency: isLayerLocked(layer, 'transparency'),\n brush,\n color: rgba,\n background: rgbaOf(doc.paper.color),\n grain: paperGrain(doc.paper),\n paperDepth: doc.paper.depth,\n seed,\n colorHex: color,\n affineCopies: copies,\n copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),\n inputFilter,\n selection,\n mixerLoad: init.load ?? brushLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]),\n secondary: rgbaOf(this.secondaryColor),\n wet,\n thick: brush.engine === 'impasto' ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : undefined,\n sticker: brush.engine === 'sticker' ? { nextId: this.nextStickerNumber(doc) } : undefined,\n paint: this.strokeGradient(init, brush, color),\n });\n if (wet) this.wet.driver = wet.driver;\n if (init.maskStencils) session.maskStencils = init.maskStencils;\n return session;\n }\n\n private record(session: PaintStrokeSession): RecordedStroke {\n const init = session.init;\n const stroke: RecordedStroke = {\n id: `s${this.strokeCounter++}`,\n layerId: session.layerId,\n brush: brushToJSON(init.brush) as PaintBrush,\n color: init.colorHex,\n points: session.points.map((p) => ({ ...p })),\n seed: init.seed,\n };\n if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m] as unknown as Affine);\n if (init.paint) stroke.paint = JSON.parse(JSON.stringify(init.paint));\n if (init.mixerLoad.reservoir || init.mixerLoad.pickup) stroke.load = JSON.parse(JSON.stringify(init.mixerLoad));\n if (init.wet) stroke.steps = [...init.wet.driver.steps];\n if (init.selection) {\n const masks = (session.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === 'mask');\n if (masks.length) stroke.stencils = masks.map((x) => ({ ...x }));\n }\n return stroke;\n }\n\n /**\n * Starts a live stroke for pointer input: `push` raw pen samples (document pixels), then `end()`\n * (one undo step) or `cancel()`.\n */\n beginStroke(init: PaintStrokeInit = {}): PaintStrokeSession {\n this.requireIdle('starting another');\n const before = this.doc;\n const host: StrokeSessionHost = {\n update: (layerId, surface, rects) => {\n if (this.session?.init.wet) {\n // The wet layer's display is the layer's pixels: show it (not an undo step).\n this.wet.syncDisplays();\n return;\n }\n this.live = { layerId, surface };\n const ch = this.session?.thickChannels();\n this.liveThick = ch ? { layerId, channels: ch } : null;\n this.emit(rects);\n },\n commit: (s, surface, cancelled) => {\n this.session = null;\n this.live = null;\n this.liveLayers = null;\n this.liveThick = null;\n const wetStroke = Boolean(s.init.wet);\n if (wetStroke) this.wet.driver = null;\n const brush = s.init.brush;\n if (!cancelled && s.result?.mixerLoad) this.keepLoad(brush, s.result.mixerLoad);\n if (cancelled || !surface) {\n if (this.wetOp) {\n // Put the wet paint back as it was before the stroke.\n const snap = this.wetOp.before;\n this.wetOp = null;\n this.pendingWetEvents = [];\n if (wetStroke) this.wet.restore(snap);\n this.doc = { ...this.doc, layers: before.layers };\n this.wet.syncDisplays();\n }\n this.emit();\n return;\n }\n const stroke = this.record(s);\n s.result!.strokeId = stroke.id;\n if (wetStroke && this.wet.instantDry) {\n this.wet.dry(s.layerId);\n this.logWet({ kind: 'dry', layerId: s.layerId, at: this.doc.strokes.length + 1 });\n surface = (this.getLayer(s.layerId) as PaintPixelLayer).surface;\n s.result!.wet = this.wet.isWet(s.layerId);\n }\n const l = this.getLayer(s.layerId) as PaintPixelLayer;\n const ch = s.thickChannels();\n const thick = ch ? { ...(this.doc.thick ?? {}), [l.id]: ch } : this.doc.thick;\n const sprayed = s.stickers();\n const stickers = sprayed ? { ...(this.doc.stickers ?? {}), [l.id]: [...(this.doc.stickers?.[l.id] ?? []), ...sprayed] } : this.doc.stickers;\n this.commit(`${brush.name} stroke`, this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...(thick ? { thick } : {}), ...(stickers ? { stickers } : {}) }), wetStroke ? before : this.doc);\n },\n };\n let session: PaintStrokeSession;\n try {\n session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);\n } catch (e) {\n this.wetOp = null;\n throw e;\n }\n this.session = session;\n this.emit([]);\n return session;\n }\n\n /** The current live stroke, if any. */\n get activeStroke(): PaintStrokeSession | null {\n return this.session;\n }\n\n /** Runs a whole stroke against `doc` without touching the editor; returns the new document. */\n private runStroke(doc: PaintDocumentState, init: PaintStrokeInit, points: readonly PenPoint[]): { doc: PaintDocumentState; result: PaintStrokeResult; brush: PaintBrush } {\n let out = doc;\n const host: StrokeSessionHost = {\n update: () => {},\n commit: (s, surface, cancelled) => {\n if (cancelled || !surface) return;\n const stroke = this.record(s);\n s.result!.strokeId = stroke.id;\n const l = findLayer(out.layers, s.layerId)!.layer as PaintPixelLayer;\n const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;\n const ch = s.thickChannels();\n const sprayed = s.stickers();\n out = {\n ...out,\n layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),\n strokes: [...out.strokes, stroke],\n ...(ch ? { thick: { ...(out.thick ?? {}), [l.id]: ch } } : {}),\n ...(sprayed ? { stickers: { ...(out.stickers ?? {}), [l.id]: [...(out.stickers?.[l.id] ?? []), ...sprayed] } } : {}),\n };\n },\n };\n const session = this.sessionFor(doc, init, host);\n session.push(points);\n const result = session.end();\n if (session.init.wet) {\n this.wet.driver = null;\n if (this.wet.instantDry && result.strokeId) {\n this.wet.dry(session.layerId);\n this.logWet({ kind: 'dry', layerId: session.layerId, at: out.strokes.length });\n const dried = (findLayer(this.doc.layers, session.layerId)!.layer as PaintPixelLayer).surface;\n out = { ...out, layers: mapLayer(out.layers, session.layerId, (l) => ({ ...(l as PaintPixelLayer), surface: dried })) };\n }\n result.wet = this.wet.isWet(session.layerId);\n }\n return { doc: out, result, brush: session.init.brush };\n }\n\n /** Paints one stroke from data (one undo step). */\n paintStroke(init: PaintStrokeInit & { points: readonly PenPoint[] }): PaintStrokeResult {\n this.requireIdle('painting');\n const points = this.checkPoints(init.points);\n const before = this.doc;\n let run: ReturnType<PaintEditorController['runStroke']>;\n try {\n run = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);\n } catch (e) {\n this.wetOp = null;\n throw e;\n }\n const { doc, result, brush } = run;\n if (result.mixerLoad) this.keepLoad(brush, result.mixerLoad);\n if (doc !== before || this.wetOp) this.commit(`${brush.name} stroke`, this.withWetEvents(doc), before);\n else this.emit([]);\n return result;\n }\n\n /** Checks stroke samples (finite x and y, pressure 0..1) and returns them; throws a sentence naming the bad point. */\n checkPoints(points: readonly PenPoint[]): PenPoint[] {\n if (!Array.isArray(points) || !points.length) throw new PaintEditorError('points is empty; give at least one {x, y} in document pixels.');\n const out: PenPoint[] = [];\n points.forEach((p, i) => {\n if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) throw new PaintEditorError(`points[${i}] needs finite x and y in document pixels.`);\n if (p.pressure !== undefined && !(p.pressure >= 0 && p.pressure <= 1)) throw new PaintEditorError(`points[${i}].pressure ${p.pressure} is out of range; use 0..1.`);\n out.push(p);\n });\n return out;\n }\n\n /** Every recorded stroke, oldest first. */\n getStrokes(): readonly RecordedStroke[] {\n return this.doc.strokes;\n }\n\n /**\n * Replays recorded strokes, varied (moved, scaled, rotated, flipped, recoloured, with another\n * brush or layer), `repeat` times: all of it one undo step. Returns the new strokes.\n */\n replayStrokes(options: ReplayOptions): { strokeIds: string[]; bounds: RasterRect | null; layerIds: string[] } {\n this.requireIdle('replaying');\n if (!options.strokeIds?.length) throw new PaintEditorError('strokeIds is empty; pass ids from list_strokes.');\n const strokes = options.strokeIds.map((id) => {\n const s = this.doc.strokes.find((x) => x.id === id);\n if (!s) throw new PaintEditorError(`Unknown stroke \"${id}\". Call list_strokes for the ids.`);\n return s;\n });\n const repeat = options.repeat ?? 1;\n if (!Number.isInteger(repeat) || repeat < 1 || repeat > 100) throw new PaintEditorError(`repeat ${repeat} is out of range; use 1..100.`);\n if (options.scale !== undefined && !(options.scale > 0 && options.scale <= 20)) throw new PaintEditorError(`scale ${options.scale} is out of range; use above 0 up to 20.`);\n if (options.color !== undefined) parseColor(options.color);\n if (options.brushId !== undefined) this.requireBrush(options.brushId);\n if (options.layerId !== undefined) this.paintTarget(options.layerId, 'replay onto');\n let origin = options.origin;\n if (!origin) {\n const b = strokeBounds(strokes.flatMap((s) => s.points));\n origin = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n }\n const step = variationAffine(options, origin);\n let doc = this.doc;\n let bounds: RasterRect | null = null;\n const ids: string[] = [];\n const layerIds = new Set<string>();\n let m: Affine = IDENTITY_AFFINE;\n for (let k = 0; k < repeat; k++) {\n m = [step[0] * m[0] + step[1] * m[2], step[0] * m[1] + step[1] * m[3], step[2] * m[0] + step[3] * m[2], step[2] * m[1] + step[3] * m[3], step[0] * m[4] + step[1] * m[5] + step[4], step[2] * m[4] + step[3] * m[5] + step[5]];\n for (const s of strokes) {\n const v = varyStroke(s, m, options);\n let brush = v.brush;\n if (options.brushId !== undefined) {\n const b = this.requireBrush(options.brushId);\n brush = normalizePaintBrush({ tip: { ...b.tip, size: b.tip.size * (v.brush.tip.size / s.brush.tip.size) } }, b);\n }\n const layerId = options.layerId ?? s.layerId;\n if (!findLayer(doc.layers, layerId)) throw new PaintEditorError(`Stroke \"${s.id}\" was painted on layer \"${layerId}\", which no longer exists; pass layerId to replay it elsewhere.`);\n // The recorded load replays the stroke exactly; a recolour reloads the reservoir with the new colour.\n let load = s.load;\n if (load && options.color !== undefined && load.reservoir) {\n const [r, g, b] = hexToRgb8(options.color);\n load = { ...load, reservoir: [r / 255, g / 255, b / 255] };\n }\n // Gradient paint replays as recorded; a recolour paints the one new colour.\n const paint = v.paint && options.color === undefined ? { paint: v.paint } : {};\n const run = this.runStroke(doc, { layerId, color: options.color ?? s.color, brush, seed: s.seed, copies: v.copies ?? [], stencils: false, maskStencils: [], ...paint, ...(load && options.brushId === undefined ? { load } : {}) }, v.points);\n doc = run.doc;\n bounds = unionRect(bounds, run.result.bounds);\n if (run.result.strokeId) ids.push(run.result.strokeId);\n layerIds.add(layerId);\n }\n }\n if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? 'Replay stroke' : 'Replay strokes', doc);\n return { strokeIds: ids, bounds, layerIds: [...layerIds] };\n }\n\n // --- Thick paint and light -------------------------------------------------------\n\n /** The document's `thick` without these layers (as a spread: `{}` when unchanged). */\n private thickWithout(ids: readonly string[]): { thick?: Record<string, ThickChannels> } {\n const t = this.doc.thick;\n if (!t || !ids.some((id) => t[id])) return {};\n const out = { ...t };\n for (const id of ids) delete out[id];\n return { thick: out };\n }\n\n /** Where the light comes from for thick paint and metallic paper. */\n getLighting(): PaintLighting {\n return { ...this.lighting };\n }\n\n /** Moves the light (thick paint and metallic paper are relit; nothing is repainted). Saved with the document, not an undo step. */\n setLighting(update: Partial<PaintLighting>): PaintLighting {\n if (update.elevation !== undefined && !(update.elevation >= 5 && update.elevation <= 90)) throw new PaintEditorError(`elevation ${update.elevation} is out of range; use 5..90 degrees.`);\n if (update.relief !== undefined && !(update.relief >= 0 && update.relief <= 1)) throw new PaintEditorError(`relief ${update.relief} is out of range; use 0..1.`);\n this.lighting = normalizeLighting(update, this.lighting);\n this.emit('all');\n return { ...this.lighting };\n }\n\n /** A layer's relief: where it is, its highest point (0..255) and its tiles; `null` when it has none. */\n reliefOf(layerId?: string): { layerId: string; bounds: RasterRect | null; maxHeight: number } | null {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) return null;\n this.requireLayer(id);\n const ch = this.doc.thick?.[id];\n if (!ch) return null;\n const r = reliefInfo(ch);\n return { layerId: id, bounds: r.bounds, maxHeight: r.maxHeight };\n }\n\n /** Flattens a layer's thick paint (its colour stays; the relief and material go). One undo step. */\n flattenRelief(layerId?: string): { layerId: string; flattened: boolean } {\n this.requireIdle('flattening');\n const l = this.paintTarget(layerId, 'flatten');\n const t = this.thickWithout([l.id]);\n if (!t.thick) return { layerId: l.id, flattened: false };\n this.commit('Flatten relief', this.withPaintEvent({ ...this.doc, ...t }, { kind: 'flatten', layerId: l.id }));\n return { layerId: l.id, flattened: true };\n }\n\n // --- Replay: time-lapse and high-resolution export ------------------------------\n\n /**\n * A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain\n * scaled; every pixel layer empty; the same brushes, lighting and wet options). Dispose of it\n * when done.\n */\n createReplayTarget(scale = 1): PaintEditorController {\n if (!(scale >= 1 && scale <= 8)) throw new PaintEditorError(`scale ${scale} is out of range; use 1..8.`);\n const { width, height, dpi } = this.doc.canvas;\n const w = Math.round(width * scale);\n const h = Math.round(height * scale);\n if (w > 16384 || h > 16384) throw new PaintEditorError(`At scale ${scale} the canvas would be ${w}×${h}; the limit is 16384 px per side. Use a smaller scale.`);\n const target = new PaintEditorController({ width: w, height: h, dpi: dpi * scale, brushes: this.userBrushes, wet: this.wetOptions });\n const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };\n target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });\n target.lighting = { ...this.lighting };\n return target;\n }\n\n /**\n * Repaints the painting from its recorded strokes and events, as a time-lapse: PNG frames (after\n * strokes, and while wet paint flows), a `VideoProject` of the frames as stills and the ffmpeg\n * command that renders it (the video editor's export path). Changes nothing here.\n */\n async timeLapse(options: TimeLapseOptions = {}): Promise<TimeLapse> {\n const target = this.createReplayTarget(1);\n try {\n return await buildTimeLapse(this.doc, target, options);\n } finally {\n target.dispose();\n }\n }\n\n /**\n * The painting repainted from its recorded strokes at `scale` times the size (brush sizes,\n * paper grain, fills and wet events scaled), wet paint dried: more detail instead of upscaled\n * pixels. Layers painted without recorded strokes (imported pixels) come out empty. Changes nothing here.\n */\n /** `renderHighRes` as PNG bytes. */\n async exportHighResPng(options: { scale: number; paper?: boolean }): Promise<Uint8Array> {\n const img = await this.renderHighRes(options);\n return encodePng(img.width, img.height, img.data);\n }\n\n async renderHighRes(options: { scale: number; paper?: boolean }): Promise<PixelImage> {\n const target = this.createReplayTarget(options.scale);\n try {\n await replayPainting(this.doc, target, { scale: options.scale });\n await target.wetIdle();\n return target.compositePixels({ paper: options.paper, wet: 'dried' });\n } finally {\n target.dispose();\n }\n }\n\n // --- Wet media -----------------------------------------------------------------\n\n /**\n * Lets wet paint flow with the wall clock: call every frame with `performance.now()` (the\n * bindings do). Steps at the wet speed unless paused; resolves when the frame's steps are done.\n */\n tickWet(now: number): Promise<void> {\n return this.wet.tick(now);\n }\n\n /** Resolves when the wet kernel is as fast as it will get here (`null` when nothing is wet yet). */\n wetReady(): Promise<WetBackendInfo | null> {\n return this.wet.world ? this.wet.world.ready : Promise.resolve(null);\n }\n\n /** Stops the wet simulation's worker threads (call when the editor goes away). */\n dispose(): void {\n this.wet.destroy();\n }\n\n /** Resolves when wet paint is between steps (edits, undo and exports can run). */\n wetIdle(): Promise<void> {\n return this.wet.idle();\n }\n\n private wetLayer(layerId: string | undefined, what: string): PaintPixelLayer {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) throw new PaintEditorError(`There is no active layer to ${what}; pass layerId (list_layers).`);\n const l = this.requireLayer(id);\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; ${what} a pixel layer inside it.`);\n return l;\n }\n\n private checkRect(rect: RasterRect | undefined): RasterRect | undefined {\n if (!rect) return undefined;\n const { width, height } = this.doc.canvas;\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) || !(rect.width > 0 && rect.height > 0)) throw new PaintEditorError('rect needs finite x, y and a width and height above 0 (document pixels).');\n const r = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.ceil(rect.width), height: Math.ceil(rect.height) };\n if (r.x >= width || r.y >= height || r.x + r.width <= 0 || r.y + r.height <= 0) throw new PaintEditorError(`rect is outside the ${width}×${height} canvas.`);\n return r;\n }\n\n /** Wet media as panels and tools show it, with each wet layer's state. */\n wetState(): PaintWetStateInfo & { layers: WetLayerInfo[]; backend: WetBackendInfo | null; message: string | null; engine: WetEngineInfo } {\n const st = this.getState().wet;\n const backend = this.wet.backend();\n return { ...st, layers: this.wet.wetLayerIds().map((id) => this.wet.info(id)), backend, message: backend?.message ?? wetSupport().message, engine: this.wet.engineInfo() };\n }\n\n /**\n * Engine B, the WebGPU accelerator for large wet areas: whether it may run, its state (`off`,\n * `not-started`, `starting`, `unavailable`, `standby`, `active`, `failed`), why, and the GPU.\n * The CPU engine stays the authority: drying, saving, export and replay never use the GPU.\n */\n wetEngine(): WetEngineInfo {\n return this.wet.engineInfo();\n }\n\n /**\n * Lets Engine B run (`gpu: 'auto'`, the default) or forces the CPU (`'off'`); `minTiles` is the\n * wet area (live 256 px tiles) at which the GPU takes over. Not an undo step and not saved with\n * the painting (it changes how wet paint is computed on this machine, never the result).\n * Resolves once the GPU has started or turned out unavailable.\n */\n async setWetEngine(options: { gpu?: WetGpuMode; minTiles?: number }): Promise<WetEngineInfo> {\n const gpu = options.gpu ?? this.wet.gpuMode;\n if (!WET_GPU_MODES.includes(gpu)) throw new PaintEditorError(`gpu must be ${WET_GPU_MODES.join(' or ')}, not ${JSON.stringify(gpu)}.`);\n if (options.minTiles !== undefined && !(Number.isInteger(options.minTiles) && options.minTiles >= 1 && options.minTiles <= 1024)) throw new PaintEditorError(`minTiles ${options.minTiles} is out of range; use 1..1024 tiles of 256 px.`);\n this.wet.setGpu(gpu, options.minTiles);\n this.emit();\n const info = await this.wet.gpuReady();\n this.emit();\n return info;\n }\n\n /** One layer's wet settings and state. */\n wetLayerInfo(layerId?: string): WetLayerInfo {\n return this.wet.info(this.wetLayer(layerId, 'read the wet paint of').id);\n }\n\n /**\n * Lets the paint flow for `steps` simulation steps now (or `seconds` at the wet speed): what\n * agents call between strokes. Not an undo step (time passing never is). Returns the steps run\n * and which layers are still wet.\n */\n async advanceWet(options: { steps?: number; seconds?: number }): Promise<{ steps: number; clock: number; stillWet: boolean; layers: WetLayerInfo[] }> {\n const steps = options.steps ?? Math.round((options.seconds ?? 1) * this.wet.speed);\n if (!(Number.isInteger(steps) && steps >= 0 && steps <= 100000)) throw new PaintEditorError(`steps ${steps} is out of range; use 0..100000 (or seconds).`);\n const run = await this.wet.advance(steps);\n this.emit();\n const st = this.wetState();\n return { steps: run, clock: st.clock, stillWet: st.wet, layers: st.layers };\n }\n\n /**\n * Dries wet paint at once: everything settles where it is and the water goes (one undo step).\n * On one layer or all, optionally only inside `rect`. Returns what dried and whether anything is\n * still wet.\n */\n dryPaint(options: { layerId?: string; rect?: RasterRect } = {}): { dried: Array<{ layerId: string; bounds: RasterRect | null }>; stillWet: boolean } {\n this.requireIdle('drying');\n this.requireWetIdle('drying');\n if (options.layerId !== undefined) this.wetLayer(options.layerId, 'dry');\n const rect = this.checkRect(options.rect);\n const ids = options.layerId !== undefined ? this.wet.wetLayerIds().filter((x) => x === options.layerId) : this.wet.wetLayerIds();\n if (!ids.length) return { dried: [], stillWet: this.wet.isWet() };\n const before = this.doc;\n this.beginWetOp();\n const dried = ids.flatMap((id) => this.wet.dry(id, rect));\n this.logWet({ kind: 'dry', layerId: options.layerId, data: rect });\n this.commit('Dry paint', this.withWetEvents(this.doc), before);\n return { dried, stillWet: this.wet.isWet() };\n }\n\n /**\n * Pre-wets the paper (wet-in-wet): fills the paper's water over `rect` (default the canvas) by\n * `amount` 0..1 on a layer's wet layer, so paint laid there spreads softly. One undo step.\n */\n wetPaper(options: { layerId?: string; rect?: RasterRect; amount?: number } = {}): { layerId: string; rect: RasterRect; wet: boolean } {\n this.requireIdle('wetting the paper');\n const l = this.wetLayer(options.layerId, 'wet the paper of');\n if (isLayerLocked(l, 'pixels')) throw new PaintEditorError(`Layer \"${l.name}\" has its pixels locked; unlock it first.`);\n const amount = options.amount ?? 1;\n if (!(amount > 0 && amount <= 1)) throw new PaintEditorError(`amount ${amount} is out of range; use above 0 up to 1.`);\n const rect = this.checkRect(options.rect) ?? { x: 0, y: 0, width: this.doc.canvas.width, height: this.doc.canvas.height };\n if (!this.wet.wetLayerIds().includes(l.id) && this.wet.wetLayerIds().length >= MAX_WET_LANES) throw new PaintEditorError(`At most ${MAX_WET_LANES} layers can hold wet paint at once; dry one first (dry_paint).`);\n const before = this.doc;\n this.beginWetOp();\n const world = this.wet.ensureLane(l.id);\n world.wetPaper(l.id, rect, null, amount);\n this.wet.syncDisplays();\n this.logWet({ kind: 'wet-paper', layerId: l.id, data: { rect, amount } });\n this.commit('Wet paper', this.withWetEvents(this.doc), before);\n return { layerId: l.id, rect, wet: this.wet.isWet(l.id) };\n }\n\n /** Records a settings change as a wet event (not an undo step). */\n private recordWetSetting(e: Omit<WetEvent, 'clock'>): void {\n this.doc = { ...this.doc, wetEvents: [...(this.doc.wetEvents ?? []), { clock: this.wet.world?.clock ?? 0, at: this.doc.strokes.length, ...e }] };\n }\n\n /** Changes a layer's wet settings (viscosity, evaporation, granulation…). Editor settings saved with the document, not an undo step. */\n setWetParams(layerId: string | undefined, update: Partial<WetParams>): WetParams {\n const l = this.wetLayer(layerId, 'change the wet settings of');\n this.requireWetIdle('changing wet settings');\n const params = this.wet.setParams(l.id, update);\n this.recordWetSetting({ kind: 'params', layerId: l.id, data: params });\n this.emit([]);\n return params;\n }\n\n /** Sets a layer's flow map (water pushed in a pattern). Saved with the document, not an undo step. */\n setFlowMap(layerId: string | undefined, update: Partial<WetFlowSpec>): WetFlowSpec {\n const l = this.wetLayer(layerId, 'set the flow map of');\n this.requireWetIdle('changing the flow map');\n const flow = this.wet.setFlow(l.id, update);\n this.recordWetSetting({ kind: 'flow', layerId: l.id, data: flow });\n this.emit([]);\n return flow;\n }\n\n /** Tilts the canvas (water runs downhill; steep tilts make drips). Saved with the document, not an undo step. */\n setCanvasTilt(update: Partial<WetTilt>): WetTilt {\n this.requireWetIdle('tilting the canvas');\n if (update.angle !== undefined && !(update.angle >= 0 && update.angle <= 90)) throw new PaintEditorError(`angle ${update.angle} is out of range; use 0 (flat) .. 90 (upright) degrees.`);\n const tilt = this.wet.setTilt(update);\n this.recordWetSetting({ kind: 'tilt', data: tilt });\n this.emit([]);\n return tilt;\n }\n\n /** Stops (or restarts) time: paused paint stays wet. Editor state. */\n setWetPaused(paused: boolean): boolean {\n this.wet.paused = paused;\n this.emit([]);\n return paused;\n }\n\n /** Simulation steps per second while time runs (1..2000; default 120). Editor state. */\n setWetSpeed(speed: number): number {\n if (!(speed >= 1 && speed <= 2000)) throw new PaintEditorError(`speed ${speed} is out of range; use 1..2000 steps per second.`);\n this.wet.speed = speed;\n this.emit([]);\n return speed;\n }\n\n /** Wet strokes dry as soon as they end (each stroke still one undo step). Editor state. */\n setInstantDry(on: boolean): boolean {\n this.wet.instantDry = on;\n this.emit([]);\n return on;\n }\n\n /** Shows or hides the water overlay (where the water is, view only). */\n setWaterOverlay(visible: boolean): boolean {\n this.wet.overlay = visible;\n this.emit([]);\n return visible;\n }\n\n /** The water overlay's tiles over a document rectangle (RGBA, view only). */\n waterOverlayTiles(rect: RasterRect): Array<{ tx: number; ty: number; data: Uint8ClampedArray }> {\n if (this.wet.busy) return [];\n return this.wet.waterOverlay(rect);\n }\n\n // --- Rendering -----------------------------------------------------------------\n\n private sources() {\n return { width: this.doc.canvas.width, height: this.doc.canvas.height };\n }\n\n /** The layers with the live stroke's surface in place. */\n private currentLayers(): PaintLayer[] {\n const live = this.live;\n if (!live) return this.doc.layers;\n if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;\n const layers = mapLayer(this.doc.layers, live.layerId, (l) => ({ ...(l as PaintPixelLayer), surface: live.surface }));\n this.liveLayers = { key: live.surface, layers };\n return layers;\n }\n\n /** View-only layers between the paper and the paint (the painting guides add the tracing image). */\n protected underlayLayers(): ViewLayer[] {\n const t = this.guides.tracing;\n if (!t || !t.visible) return [];\n return [this.tracingNode(t)];\n }\n\n /** The tracing image resampled onto the canvas (cached until it or the canvas changes). */\n private tracingNode(t: TracingImage): ImageNode {\n const { width, height } = this.doc.canvas;\n const key = this.tracingVersion * 100003 + width * 7 + height;\n if (this.tracingCache?.key === key && this.tracingCache.node.opacity === t.opacity) return this.tracingCache.node;\n const src = this.imagePixels.get(t.src) ?? this.decodeImage(t.src);\n const image = resampleInto(src, t.rect, width, height);\n const node: ImageNode = { type: 'image', id: TRACING_NODE_ID, name: 'Tracing', visible: true, opacity: t.opacity, blend: 'normal', image, version: key + Math.round(t.opacity * 1000) / 1e4 };\n this.tracingCache = { key, node };\n return node;\n }\n\n /** The layers with every wet layer dried where it is (what saving and exporting see). */\n private driedLayers(layers: PaintLayer[] = this.currentLayers()): PaintLayer[] {\n const ids = this.wet.wetLayerIds();\n if (!ids.length) return layers;\n this.requireWetIdle('exporting');\n let out = layers;\n for (const id of ids) {\n const baked = this.wet.bakedSurface(id);\n if (baked && findLayer(out, id)) out = mapLayer(out, id, (l) => ({ ...(l as PaintPixelLayer), surface: baked }));\n }\n return out;\n }\n\n /** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */\n private litLayers(layers: PaintLayer[]): PaintLayer[] {\n const thick = this.doc.thick;\n const live = this.liveThick;\n if (!thick && !live) return layers;\n let out = layers;\n const ids = new Set([...Object.keys(thick ?? {}), ...(live ? [live.layerId] : [])]);\n for (const id of ids) {\n const ch = live?.layerId === id ? live.channels : thick?.[id];\n const l = ch && findLayer(out, id)?.layer;\n if (!ch || !isPixelLayer(l)) continue;\n let cache = this.litCaches.get(id);\n if (!cache) this.litCaches.set(id, (cache = new LitCache()));\n const lit = cache.lit(l.surface, ch, this.lighting);\n if (lit !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: lit }));\n }\n return out;\n }\n\n private renderLayers(options: RenderOptions, view = false): ViewLayer[] {\n let layers: PaintLayer[] = this.litLayers((options.wet ?? 'display') === 'dried' ? this.driedLayers() : this.currentLayers());\n if (options.layerIds) {\n const picked: PaintLayer[] = [];\n for (const id of options.layerIds) picked.push(findLayer(layers, id)?.layer ?? this.requireLayer(id));\n layers = picked.map((l) => ({ ...l, visible: true }) as PaintLayer);\n }\n const under: ViewLayer[] = options.paper === false ? [] : [paperNode(this.doc.paper, this.lighting)];\n if (view) under.push(...this.underlayLayers());\n return [...under, ...layers];\n }\n\n /**\n * Composites the viewport stack (paper, view-only underlays, layers with the live stroke) tile by\n * tile over `rect`, calling `onTile` with each 256 px tile's RGBA (cached: unchanged tiles come back\n * as the same buffer). Pass `takeDirty()` as `dirty`.\n */\n compositeTiles(rect: RasterRect, onTile: (tx: number, ty: number, data: Uint8ClampedArray, changed: boolean) => void, options: { dirty?: RasterRect[] | 'all' } = {}): void {\n const dirty = options.dirty === 'all' ? undefined : options.dirty;\n this.viewCompositor.compositeTiles(this.renderLayers({}, true), rect, this.sources(), onTile, { dirty, checkpointLayerId: this.live?.layerId ?? this.doc.activeLayerId });\n }\n\n /** The canvas as canvas-sized RGBA (paper included by default). */\n compositePixels(options: RenderOptions = {}): PixelImage {\n const { width, height } = this.doc.canvas;\n const data = this.exportCompositor.composite(this.renderLayers({ ...options, wet: options.wet ?? 'dried' }), { x: 0, y: 0, width, height }, this.sources());\n return { width, height, data };\n }\n\n /** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */\n renderPreview(options: RenderOptions & { maxSide?: number } = {}): PixelImage {\n const full = this.compositePixels({ ...options, wet: options.wet ?? 'display' });\n const side = Math.max(1, Math.round(options.maxSide ?? 512));\n const k = Math.min(1, side / Math.max(full.width, full.height));\n if (k >= 1) return full;\n const w = Math.max(1, Math.round(full.width * k));\n const h = Math.max(1, Math.round(full.height * k));\n return { width: w, height: h, data: downsampleRgba(full.data, full.width, full.height, w, h) };\n }\n\n // --- Save, load and export -------------------------------------------------------\n\n /** The document as JSON with inline PNG assets. */\n /** The document as saved: wet paint dried where it is (deterministically); wet settings as data. */\n private savedState(): PaintDocumentState {\n const layers = this.driedLayers(this.doc.layers);\n return layers === this.doc.layers ? this.doc : { ...this.doc, layers };\n }\n\n /** Wet settings saved with the document (`null` when they are all defaults). */\n private wetJSON(): unknown {\n const layers: Record<string, WetLayerSettings> = {};\n for (const [id, set] of this.wet.settings) if (findLayer(this.doc.layers, id)) layers[id] = { params: { ...set.params }, flow: { ...set.flow } };\n const flat = this.wet.tilt.angle === 0;\n if (!Object.keys(layers).length && flat) return undefined;\n return { tilt: { ...this.wet.tilt }, layers };\n }\n\n private readWet(raw: unknown, problems: string[]): void {\n this.wet.reset();\n if (!raw || typeof raw !== 'object') return;\n const r = raw as { tilt?: unknown; layers?: unknown };\n this.wet.setTilt(normalizeTilt((r.tilt as Partial<WetTilt>) ?? {}));\n if (r.layers && typeof r.layers === 'object') {\n for (const [id, v] of Object.entries(r.layers as Record<string, unknown>)) {\n if (!v || typeof v !== 'object') {\n problems.push(`wet.layers[\"${id}\"] is not wet settings; skipped.`);\n continue;\n }\n const x = v as { params?: Partial<WetParams>; flow?: Partial<WetFlowSpec> };\n this.wet.settings.set(id, { params: normalizeWetParams(x.params ?? {}), flow: normalizeFlow(x.flow ?? {}) });\n }\n }\n }\n\n /** Thick paint and light as saved (`layerChannels`, `lighting`), merged into the extensions. */\n private thickExtensions(): PaintDocumentExtensions {\n const ext: PaintDocumentExtensions = { ...this.extensions };\n const channels: Record<string, unknown> = {};\n for (const [id, ch] of Object.entries(this.doc.thick ?? {})) {\n if (!findLayer(this.doc.layers, id)) continue;\n const saved = thickToJSON(ch);\n if (saved) channels[id] = saved;\n }\n if (Object.keys(channels).length) ext.layerChannels = channels;\n else delete ext.layerChannels;\n const stickers: Record<string, Sticker[]> = {};\n for (const [id, list] of Object.entries(this.doc.stickers ?? {})) if (list.length && findLayer(this.doc.layers, id)) stickers[id] = list.map((x) => ({ ...x }));\n if (Object.keys(stickers).length) ext.stickers = stickers;\n else delete ext.stickers;\n if (this.lighting.azimuth !== DEFAULT_LIGHTING.azimuth || this.lighting.elevation !== DEFAULT_LIGHTING.elevation || this.lighting.relief !== DEFAULT_LIGHTING.relief) ext.lighting = { ...this.lighting };\n else delete ext.lighting;\n return ext;\n }\n\n /** Reads `layerChannels` and `lighting` from loaded extensions into the document. */\n private readThick(state: PaintDocumentState, ext: PaintDocumentExtensions, problems: string[]): PaintDocumentState {\n this.lighting = normalizeLighting((ext.lighting as Partial<PaintLighting>) ?? {}, DEFAULT_LIGHTING);\n this.litCaches.clear();\n if (ext.stickers && typeof ext.stickers === 'object') {\n const stickers: Record<string, Sticker[]> = {};\n for (const [id, list] of Object.entries(ext.stickers as Record<string, unknown>)) {\n if (!findLayer(state.layers, id) || !Array.isArray(list)) continue;\n const ok = (list as Sticker[]).filter((x) => x && typeof x.id === 'string' && STAMP_KINDS.includes(x.stamp) && Number.isFinite(x.x) && Number.isFinite(x.y) && x.size > 0);\n if (ok.length !== list.length) problems.push(`Some stickers of layer \"${id}\" could not be read; they were dropped.`);\n stickers[id] = ok.map((x) => ({ ...x }));\n }\n if (Object.keys(stickers).length) state = { ...state, stickers };\n }\n const raw = ext.layerChannels;\n if (!raw || typeof raw !== 'object') return state;\n const thick: Record<string, ThickChannels> = {};\n for (const [id, v] of Object.entries(raw as Record<string, unknown>)) {\n if (!findLayer(state.layers, id)) continue;\n try {\n thick[id] = thickFromJSON(v);\n } catch (e) {\n problems.push(`The thick paint of layer \"${id}\" could not be read (${(e as Error).message}); it loads flat.`);\n }\n }\n return Object.keys(thick).length ? { ...state, thick } : state;\n }\n\n toJSON(options: { strokes?: boolean } = {}): SerializedPaintDocument {\n return serializePaintDocument(this.savedState(), { strokes: options.strokes, extensions: this.thickExtensions(), guides: this.guides, swatches: this.swatches, mixingPad: this.mixingPadJSON(), wet: this.wetJSON() });\n }\n\n /** The document as JSON with each PNG stored in `store`. */\n toJSONExternal(store: PaintAssetStore, options: { strokes?: boolean } = {}): Promise<SerializedPaintDocument> {\n return serializePaintDocumentExternal(this.savedState(), store, { strokes: options.strokes, extensions: this.thickExtensions(), guides: this.guides, swatches: this.swatches, mixingPad: this.mixingPadJSON(), wet: this.wetJSON() });\n }\n\n private adopt(loaded: { state: PaintDocumentState; extensions: PaintDocumentExtensions; problems: string[]; guides?: unknown; swatches?: unknown; mixingPad?: unknown; wet?: unknown }): string[] {\n this.extensions = loaded.extensions;\n this.readWet(loaded.wet, loaded.problems);\n this.readMixingPad(loaded.mixingPad, loaded.problems);\n this.readGuides(loaded.guides, loaded.state, loaded.problems);\n if (Array.isArray(loaded.swatches)) this.swatches = loaded.swatches.map(readSwatch).filter((x): x is Swatch => x !== null);\n for (const s of loaded.state.strokes) {\n const n = Number(/^s(\\d+)$/.exec(s.id)?.[1] ?? 0);\n if (n >= this.strokeCounter) this.strokeCounter = n + 1;\n }\n this.undoStack = [];\n this.redoStack = [];\n this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);\n delete this.extensions.layerChannels;\n delete this.extensions.lighting;\n delete this.extensions.stickers;\n this.viewCompositor.clear();\n this.emit();\n return loaded.problems;\n }\n\n /** Opens a saved document (clears the history). Returns what could not be read. */\n loadJSON(raw: unknown): string[] {\n this.requireIdle('opening a document');\n try {\n return this.adopt(deserializePaintDocument(raw));\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** Opens a saved document whose assets may live in `store`. */\n async loadJSONAsync(raw: unknown, store?: PaintAssetStore): Promise<string[]> {\n this.requireIdle('opening a document');\n try {\n return this.adopt(await deserializePaintDocumentAsync(raw, store));\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** The canvas as PNG bytes (paper included by default; `scale` below 1 shrinks it). */\n exportPng(options: RenderOptions & { scale?: number } = {}): Uint8Array {\n const img = this.scaled(options);\n return encodePng(img.width, img.height, img.data);\n }\n\n private scaled(options: RenderOptions & { scale?: number }): PixelImage {\n const full = this.compositePixels(options);\n const s = options.scale ?? 1;\n if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);\n if (s === 1) return full;\n const w = Math.max(1, Math.round(full.width * s));\n const h = Math.max(1, Math.round(full.height * s));\n return { width: w, height: h, data: downsampleRgba(full.data, full.width, full.height, w, h) };\n }\n\n /**\n * The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.\n * JPEG and WebP need a canvas (a browser or Worker) or the `encodeImage` option.\n */\n async exportImage(options: RenderOptions & { format?: ImageFormat; quality?: number; scale?: number } = {}): Promise<Uint8Array> {\n const format = options.format ?? 'image/png';\n if (format === 'image/png') return this.exportPng(options);\n if (format !== 'image/jpeg' && format !== 'image/webp') throw new PaintEditorError(`format ${JSON.stringify(format)} is not supported; use image/png, image/jpeg or image/webp.`);\n const quality = options.quality ?? 0.92;\n if (!(quality >= 0 && quality <= 1)) throw new PaintEditorError(`quality ${quality} is out of range; use 0..1.`);\n const img = this.scaled({ ...options, paper: format === 'image/jpeg' ? true : options.paper });\n const encode = this.encodeImage ?? defaultImageEncoder();\n if (!encode) throw new PaintEditorError(`${format} needs a canvas to encode and there is none here; pass encodeImage to the controller, or export PNG.`);\n return encode(img, format, quality);\n }\n\n /**\n * The document as an 8-bit RGB PSD through the shared PSD writer: every layer and group, with\n * the paper as a locked \"Paper\" pixel layer at the bottom (it is generated, so PSD gets its\n * pixels), and the composite as the merged image.\n */\n exportPsd(): PsdExportResult {\n const dried = this.litLayers(this.driedLayers(this.doc.layers));\n const { width, height, dpi } = this.doc.canvas;\n const paperPixels = this.compositePixels({ layerIds: [], paper: true });\n const paperLayer: PaintPixelLayer = {\n type: 'pixel',\n id: 'paper',\n name: 'Paper',\n visible: true,\n opacity: 1,\n blend: 'normal',\n locks: { all: true },\n surface: surfaceFromPixels(paperPixels, 4),\n offset: { x: 0, y: 0 },\n };\n const merged = this.compositePixels({ paper: true }).data;\n // PSD has no pigment or painterly modes: write each as its nearest standard mode and say so.\n const changed = new Map<BlendMode, string[]>();\n const toStandard = (layers: readonly PaintLayer[]): PaintLayer[] =>\n layers.map((l) => {\n const blend = l.blend === 'pass-through' ? l.blend : standardBlendMode(l.blend as BlendMode);\n if (blend !== l.blend) changed.set(l.blend as BlendMode, [...(changed.get(l.blend as BlendMode) ?? []), l.name]);\n const next = blend === l.blend ? l : ({ ...l, blend } as PaintLayer);\n return next.type === 'group' ? { ...next, children: toStandard(next.children) } : next;\n });\n const layers = toStandard(dried);\n const result = exportPsd({ width, height, layers: [paperLayer, ...layers] as unknown as ImageLayer[], merged, resolution: dpi });\n for (const [mode, names] of changed) {\n const label = BLEND_MODES.find((m) => m.id === mode)?.label ?? mode;\n const std = BLEND_MODES.find((m) => m.id === standardBlendMode(mode))?.label ?? standardBlendMode(mode);\n const list = names.map((n) => `\"${n}\"`).join(', ');\n const one = names.length === 1;\n result.warnings.push(`${label} blending has no PSD equivalent; ${list} ${one ? 'was' : 'were'} written as ${std}, the nearest Photoshop mode, so ${one ? 'it looks' : 'they look'} different in Photoshop (the merged image is exact).`);\n }\n return result;\n }\n\n /** `render_preview` as a base64 PNG. */\n previewPngBase64(options: RenderOptions & { maxSide?: number } = {}): { data: string; width: number; height: number } {\n const img = this.renderPreview(options);\n return { data: bytesToBase64(encodePng(img.width, img.height, img.data)), width: img.width, height: img.height };\n }\n\n // --- Guides --------------------------------------------------------------------\n\n private setGuides(next: PaintGuides): PaintGuides {\n this.guides = next;\n this.emit('all');\n return next;\n }\n\n /** Mirror and kaleidoscope painting: every new stroke paints its copies too (one stroke, one undo step). */\n setSymmetry(update: Partial<SymmetryGuide>): SymmetryGuide {\n const symmetry = normalizeSymmetry(update, this.guides.symmetry);\n this.setGuides({ ...this.guides, symmetry });\n return symmetry;\n }\n\n /** Perspective guides with one to three vanishing points that pointer strokes snap to. */\n setPerspective(update: Partial<PerspectiveGuide>): PerspectiveGuide {\n let perspective: PerspectiveGuide;\n try {\n perspective = normalizePerspective(update, this.guides.perspective);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.setGuides({ ...this.guides, perspective });\n return perspective;\n }\n\n /** An image from a PNG (base64 or data URL) or from RGBA pixels, stored as a PNG data URL. */\n private importImage(input: { png?: string; pixels?: PixelImage }, what: string): { src: string; pixels: PixelImage } {\n let pixels: PixelImage;\n if (input.pixels) {\n const { width, height, data } = input.pixels;\n if (!(width > 0 && height > 0 && Number.isInteger(width) && Number.isInteger(height)) || data.length !== width * height * 4) {\n throw new PaintEditorError(`${what}: pixels must be width × height × 4 bytes of RGBA.`);\n }\n pixels = { width, height, data: new Uint8ClampedArray(data) };\n } else if (typeof input.png === 'string' && input.png.trim()) {\n pixels = this.decodeImage(input.png.trim().startsWith('data:') ? input.png.trim() : `data:image/png;base64,${input.png.trim()}`, what);\n } else throw new PaintEditorError(`${what}: pass png (a PNG as base64 or a data URL) or pixels.`);\n if (pixels.width > MAX_IMAGE_SIDE || pixels.height > MAX_IMAGE_SIDE) throw new PaintEditorError(`${what} is ${pixels.width}×${pixels.height}; the limit is ${MAX_IMAGE_SIDE} px per side.`);\n const src = pngDataUrl(pixels.width, pixels.height, pixels.data);\n this.imagePixels.set(src, pixels);\n return { src, pixels };\n }\n\n private decodeImage(src: string, what = 'Image'): PixelImage {\n const cached = this.imagePixels.get(src);\n if (cached) return cached;\n const parsed = parseDataUrl(src);\n if (!parsed) throw new PaintEditorError(`${what} is not a data URL or base64 PNG.`);\n try {\n const png = decodePng(parsed.bytes);\n const px = { width: png.width, height: png.height, data: png.data };\n this.imagePixels.set(src, px);\n return px;\n } catch (e) {\n throw new PaintEditorError(`${what} could not be decoded as a PNG (${(e as Error).message}); decode other formats in the browser and pass pixels.`);\n }\n }\n\n /**\n * Puts an image under the paint to trace over: view only, never in exports. Placed to fit the\n * canvas unless `rect` is given; `sampleColors` makes strokes take their colour from it.\n */\n setTracingImage(options: { png?: string; pixels?: PixelImage; rect?: TracingImage['rect']; opacity?: number; sampleColors?: boolean }): TracingImage {\n const { src, pixels } = this.importImage(options, 'The tracing image');\n const { width, height } = this.doc.canvas;\n const k = Math.min(width / pixels.width, height / pixels.height);\n const fit = { x: (width - pixels.width * k) / 2, y: (height - pixels.height * k) / 2, width: pixels.width * k, height: pixels.height * k };\n const tracing: TracingImage = { src, width: pixels.width, height: pixels.height, rect: fit, opacity: 0.35, visible: true, sampleColors: false };\n return this.updateTracing({ ...options, rect: options.rect ?? fit }, tracing);\n }\n\n /** Changes the tracing image's opacity, visibility, placement or colour sampling. */\n updateTracing(update: Partial<Pick<TracingImage, 'opacity' | 'visible' | 'sampleColors' | 'rect'>>, base = this.guides.tracing): TracingImage {\n if (!base) throw new PaintEditorError('There is no tracing image; set one with set_tracing_image first.');\n if (update.opacity !== undefined && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);\n if (update.rect && !(update.rect.width > 0 && update.rect.height > 0)) throw new PaintEditorError('rect needs a width and height above 0.');\n const tracing: TracingImage = {\n ...base,\n ...(update.opacity !== undefined ? { opacity: update.opacity } : {}),\n ...(update.visible !== undefined ? { visible: update.visible } : {}),\n ...(update.sampleColors !== undefined ? { sampleColors: update.sampleColors } : {}),\n ...(update.rect ? { rect: { ...update.rect } } : {}),\n };\n if (tracing.src !== base.src || update.rect) this.tracingVersion++;\n if (base !== this.guides.tracing) this.tracingVersion++;\n this.setGuides({ ...this.guides, tracing });\n return tracing;\n }\n\n clearTracingImage(): void {\n this.tracingVersion++;\n this.setGuides({ ...this.guides, tracing: null });\n }\n\n /** The tracing image's colour at a document pixel, or `null` outside it or where it is clear. */\n sampleTracingColor(x: number, y: number): string | null {\n const t = this.guides.tracing;\n if (!t) return null;\n const src = this.decodeImage(t.src);\n const u = Math.floor(((x - t.rect.x) / t.rect.width) * src.width);\n const v = Math.floor(((y - t.rect.y) / t.rect.height) * src.height);\n if (u < 0 || v < 0 || u >= src.width || v >= src.height) return null;\n const i = (v * src.width + u) * 4;\n if (src.data[i + 3] === 0) return null;\n return rgbToHex(src.data[i], src.data[i + 1], src.data[i + 2]);\n }\n\n /** Adds a floating reference image to look at and pick colours from. */\n addReferenceImage(options: { png?: string; pixels?: PixelImage; name?: string; x?: number; y?: number; displayWidth?: number }): ReferenceImage {\n const { src, pixels } = this.importImage(options, 'The reference image');\n const ref: ReferenceImage = {\n id: `ref${this.nextReferenceId++}`,\n name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,\n src,\n width: pixels.width,\n height: pixels.height,\n x: options.x ?? 16 + this.guides.references.length * 24,\n y: options.y ?? 16 + this.guides.references.length * 24,\n displayWidth: options.displayWidth ?? Math.min(240, pixels.width),\n visible: true,\n };\n this.setGuides({ ...this.guides, references: [...this.guides.references, ref] });\n return ref;\n }\n\n private requireReference(id: string): ReferenceImage {\n const r = this.guides.references.find((x) => x.id === id);\n if (!r) throw new PaintEditorError(`Unknown reference image \"${id}\". Call list_reference_images for the ids.`);\n return r;\n }\n\n /** Moves, resizes, renames, shows or hides a reference image. */\n updateReferenceImage(id: string, update: Partial<Pick<ReferenceImage, 'name' | 'x' | 'y' | 'displayWidth' | 'visible'>>): ReferenceImage {\n const r = this.requireReference(id);\n if (update.displayWidth !== undefined && !(update.displayWidth >= 16 && update.displayWidth <= 4096)) throw new PaintEditorError(`displayWidth ${update.displayWidth} is out of range; use 16..4096.`);\n const next: ReferenceImage = { ...r, ...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== undefined)) };\n this.setGuides({ ...this.guides, references: this.guides.references.map((x) => (x.id === id ? next : x)) });\n return next;\n }\n\n removeReferenceImage(id: string): void {\n this.requireReference(id);\n this.setGuides({ ...this.guides, references: this.guides.references.filter((x) => x.id !== id) });\n }\n\n /** A reference image's pixels (decoded once). */\n referencePixels(id: string): PixelImage {\n return this.decodeImage(this.requireReference(id).src);\n }\n\n /** The colour of a reference image at its pixel (x, y). */\n pickReferenceColor(id: string, x: number, y: number): string {\n const r = this.requireReference(id);\n const px = this.decodeImage(r.src);\n const u = Math.floor(x);\n const v = Math.floor(y);\n if (!(u >= 0 && v >= 0 && u < px.width && v < px.height)) throw new PaintEditorError(`(${x}, ${y}) is outside reference \"${r.name}\" (${px.width}×${px.height}); use its own pixel coordinates.`);\n const i = (v * px.width + u) * 4;\n return rgbToHex(px.data[i], px.data[i + 1], px.data[i + 2]);\n }\n\n private readGuides(raw: unknown, state: PaintDocumentState, problems: string[]): void {\n const base = defaultGuides(state.canvas);\n this.guides = base;\n this.tracingVersion++;\n if (!raw || typeof raw !== 'object') return;\n const r = raw as Record<string, unknown>;\n const guides: PaintGuides = { ...base };\n guides.symmetry = normalizeSymmetry((r.symmetry as Record<string, unknown>) ?? {}, base.symmetry);\n try {\n guides.perspective = normalizePerspective((r.perspective as Record<string, unknown>) ?? {}, base.perspective);\n } catch (e) {\n problems.push(`Perspective guide: ${(e as Error).message}`);\n }\n const t = r.tracing as TracingImage | null | undefined;\n if (t && typeof t.src === 'string') {\n try {\n const px = this.decodeImage(t.src, 'The tracing image');\n guides.tracing = {\n src: t.src,\n width: px.width,\n height: px.height,\n rect: t.rect && t.rect.width > 0 && t.rect.height > 0 ? { ...t.rect } : { x: 0, y: 0, width: state.canvas.width, height: state.canvas.height },\n opacity: typeof t.opacity === 'number' ? Math.min(1, Math.max(0, t.opacity)) : 0.35,\n visible: t.visible !== false,\n sampleColors: t.sampleColors === true,\n };\n } catch (e) {\n problems.push((e as Error).message);\n }\n }\n if (Array.isArray(r.references)) {\n for (const ref of r.references as ReferenceImage[]) {\n if (!ref || typeof ref.src !== 'string' || typeof ref.id !== 'string') continue;\n try {\n const px = this.decodeImage(ref.src, `Reference \"${ref.name}\"`);\n guides.references.push({ id: ref.id, name: String(ref.name ?? ref.id), src: ref.src, width: px.width, height: px.height, x: Number(ref.x) || 0, y: Number(ref.y) || 0, displayWidth: Number(ref.displayWidth) || 240, visible: ref.visible !== false });\n const n = Number(/^ref(\\d+)$/.exec(ref.id)?.[1] ?? 0);\n if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;\n } catch (e) {\n problems.push((e as Error).message);\n }\n }\n }\n if (Array.isArray(r.stencils)) {\n guides.stencils = [];\n (r.stencils as Stencil[]).forEach((raw, i) => {\n try {\n const id = typeof raw?.id === 'string' && /^stencil\\d+$/.test(raw.id) ? raw.id : `stencil${this.nextStencilId}`;\n guides.stencils.push(normalizeStencil(raw, id));\n const n = Number(id.slice(7));\n if (n >= this.nextStencilId) this.nextStencilId = n + 1;\n } catch (e) {\n problems.push(`Stencil ${i + 1}: ${(e as Error).message}`);\n }\n });\n }\n this.guides = guides;\n }\n\n // --- Stickers --------------------------------------------------------------------\n\n /** The document's `stickers` without these layers (as a spread: `{}` when unchanged). */\n private stickersWithout(ids: readonly string[]): { stickers?: Record<string, Sticker[]> } {\n const t = this.doc.stickers;\n if (!t || !ids.some((id) => t[id])) return {};\n const out = { ...t };\n for (const id of ids) delete out[id];\n return { stickers: out };\n }\n\n /** The next free sticker number (ids `st<n>` are unique across the document). */\n private nextStickerNumber(doc: PaintDocumentState = this.doc): number {\n let n = 0;\n for (const list of Object.values(doc.stickers ?? {})) for (const st of list) n = Math.max(n, Number(/^st(\\d+)$/.exec(st.id)?.[1] ?? 0));\n return n + 1;\n }\n\n private stickerLayer(layerId: string | undefined): { layer: PaintPixelLayer; stickers: Sticker[] } {\n const l = this.wetLayer(layerId, 'edit the stickers of');\n const stickers = this.doc.stickers?.[l.id];\n if (!stickers) throw new PaintEditorError(`Layer \"${l.name}\" has no stickers; spray some with a sticker brush (list_brushes category sticker).`);\n return { layer: l, stickers };\n }\n\n /** A layer's stickers, bottom first. */\n listStickers(layerId?: string): Sticker[] {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) return [];\n this.requireLayer(id);\n return (this.doc.stickers?.[id] ?? []).map((s) => ({ ...s }));\n }\n\n /** Replaces a layer's stickers and redraws its pixels from them (one undo step). */\n private commitStickers(label: string, layer: PaintPixelLayer, stickers: Sticker[]): void {\n if (isLayerLocked(layer, 'pixels')) throw new PaintEditorError(`Layer \"${layer.name}\" has its pixels locked; unlock it first.`);\n const surface = stickerSurface(stickers);\n const doc = { ...this.doc, layers: mapLayer(this.doc.layers, layer.id, () => ({ ...layer, surface, offset: { x: 0, y: 0 } })), stickers: { ...(this.doc.stickers ?? {}), [layer.id]: stickers } };\n this.commit(label, this.withPaintEvent(doc, { kind: 'stickers', layerId: layer.id, data: { stickers: stickers.map((s) => ({ ...s })) } }));\n }\n\n /** Moves, turns, resizes, recolours or restamps one sticker (one undo step). Returns it. */\n updateSticker(layerId: string | undefined, id: string, update: Partial<Omit<Sticker, 'id'>>): Sticker {\n const { layer, stickers } = this.stickerLayer(layerId);\n const i = stickers.findIndex((s) => s.id === id);\n if (i < 0) throw new PaintEditorError(`Layer \"${layer.name}\" has no sticker \"${id}\". Call list_stickers for the ids.`);\n let u: Partial<Omit<Sticker, 'id'>>;\n try {\n u = normalizeStickerUpdate(update);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n const next = { ...stickers[i], ...u };\n this.commitStickers('Edit sticker', layer, stickers.map((s, j) => (j === i ? next : s)));\n return { ...next };\n }\n\n /** Brings stickers to the top (`front`) or bottom of their layer (one undo step). */\n arrangeStickers(layerId: string | undefined, ids: readonly string[], to: 'front' | 'back'): string[] {\n const { layer, stickers } = this.stickerLayer(layerId);\n for (const id of ids) if (!stickers.some((s) => s.id === id)) throw new PaintEditorError(`Layer \"${layer.name}\" has no sticker \"${id}\". Call list_stickers for the ids.`);\n const picked = stickers.filter((s) => ids.includes(s.id));\n const rest = stickers.filter((s) => !ids.includes(s.id));\n const next = to === 'front' ? [...rest, ...picked] : [...picked, ...rest];\n this.commitStickers('Arrange stickers', layer, next);\n return next.map((s) => s.id);\n }\n\n /** Deletes stickers (`ids`, or all of them on the layer) (one undo step). Returns the ids removed. */\n removeStickers(layerId: string | undefined, ids?: readonly string[]): string[] {\n const { layer, stickers } = this.stickerLayer(layerId);\n if (ids) for (const id of ids) if (!stickers.some((s) => s.id === id)) throw new PaintEditorError(`Layer \"${layer.name}\" has no sticker \"${id}\". Call list_stickers for the ids.`);\n const removed = ids ? [...ids] : stickers.map((s) => s.id);\n this.commitStickers('Remove stickers', layer, stickers.filter((s) => !removed.includes(s.id)));\n return removed;\n }\n\n /** Sets a layer's stickers (replays). */\n replaceStickers(layerId: string, stickers: readonly Sticker[]): void {\n const l = this.wetLayer(layerId, 'set the stickers of');\n this.commitStickers('Stickers', l, stickers.map((s) => ({ ...s })));\n }\n\n // --- Stencils --------------------------------------------------------------------\n\n private stencilCache: { key: unknown; size: string; mask: { width: number; height: number; data: Uint8Array } | null } | null = null;\n private nextStencilId = 1;\n\n /** The mask stencils' coverage over the canvas (cached), or `null` when none masks. */\n private stencilSelection(stencils: readonly Stencil[] = this.guides.stencils): { width: number; height: number; data: Uint8Array } | null {\n const { width, height } = this.doc.canvas;\n const size = `${width}x${height}`;\n if (this.stencilCache?.key === stencils && this.stencilCache.size === size) return this.stencilCache.mask;\n const mask = stencilMask(stencils, width, height);\n this.stencilCache = { key: stencils, size, mask };\n return mask;\n }\n\n /** The stencils. */\n listStencils(): Stencil[] {\n return this.guides.stencils.map((s) => ({ ...s }));\n }\n\n /** Lays a stencil on the canvas (a ruler or arc to follow, a straight edge or shape to mask). Editor state saved with the document, not an undo step. */\n addStencil(input: StencilInput): Stencil {\n let stencil: Stencil;\n try {\n stencil = normalizeStencil(input, `stencil${this.nextStencilId++}`);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.setGuides({ ...this.guides, stencils: [...this.guides.stencils, stencil] });\n return { ...stencil };\n }\n\n private requireStencil(id: string): Stencil {\n const s = this.guides.stencils.find((x) => x.id === id);\n if (!s) throw new PaintEditorError(`Unknown stencil \"${id}\". Call get_guides for the stencil ids.`);\n return s;\n }\n\n /** Moves, reshapes, switches (guide / mask, invert) or hides a stencil. */\n updateStencil(id: string, update: Partial<Omit<Stencil, 'id' | 'kind'>>): Stencil {\n const base = this.requireStencil(id);\n let next: Stencil;\n try {\n next = normalizeStencil({ ...update, kind: base.kind }, id, base);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.setGuides({ ...this.guides, stencils: this.guides.stencils.map((x) => (x.id === id ? next : x)) });\n return { ...next };\n }\n\n /** Takes stencils away (`ids`, or all of them). Returns the ids removed. */\n removeStencils(ids?: readonly string[]): string[] {\n if (ids) for (const id of ids) this.requireStencil(id);\n const removed = ids ? [...ids] : this.guides.stencils.map((s) => s.id);\n this.setGuides({ ...this.guides, stencils: this.guides.stencils.filter((s) => !removed.includes(s.id)) });\n return removed;\n }\n\n // --- Swatches and harmonies ------------------------------------------------------\n\n /** Colour harmonies around a colour (default the paint colour) on the painter's RYB wheel or the RGB wheel. */\n harmony(kind: HarmonyKind, options: { color?: string; wheel?: ColorWheel; spread?: number } = {}): HarmonyColor[] {\n try {\n return colorHarmony(options.color === undefined ? this.color : parseColor(options.color), kind, options);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** Adds a swatch: a colour, or a paint source (a gradient swatch holds its gradient). */\n addSwatch(color: string | PaintSource, name?: string): Swatch[] {\n this.swatches = [...this.swatches, this.makeSwatch(color, name)];\n this.emit([]);\n return this.swatches;\n }\n\n private makeSwatch(color: string | PaintSource, name?: string): Swatch {\n const p = typeof color === 'string' ? ({ type: 'color', color: parseColor(color) } as PaintSource) : this.readStrokePaint(color);\n const out: Swatch = { color: paintSourceColor(p, { paint: this.color, secondary: this.secondaryColor }) };\n if (name?.trim()) out.name = name.trim();\n if (p.type === 'gradient') out.paint = p;\n return out;\n }\n\n removeSwatch(index: number): Swatch[] {\n if (!Number.isInteger(index) || index < 0 || index >= this.swatches.length) throw new PaintEditorError(`index ${index} is out of range; there are ${this.swatches.length} swatches (0-based).`);\n this.swatches = this.swatches.filter((_, i) => i !== index);\n this.emit([]);\n return this.swatches;\n }\n\n setSwatches(swatches: readonly Swatch[]): Swatch[] {\n this.swatches = swatches.map((x) => this.makeSwatch(x.paint ?? x.color, x.name));\n this.emit([]);\n return this.swatches;\n }\n\n // --- Mixing pad ------------------------------------------------------------------\n\n get mixingPad(): MixingPad {\n return this.pad;\n }\n\n /** Paints (`paint`) or smears with the palette knife (`knife`) on the mixing pad, in pad pixels. */\n mixingPadStroke(points: readonly PenPoint[], options: { tool?: MixingPadTool; color?: string; size?: number } = {}): { bounds: RasterRect | null } {\n const color = options.color === undefined ? this.color : parseColor(options.color);\n if (options.size !== undefined && !(options.size >= 1 && options.size <= 200)) throw new PaintEditorError(`size ${options.size} is out of range; use 1..200 px.`);\n try {\n const r = this.pad.stroke(points, { tool: options.tool ?? 'paint', color, size: options.size });\n this.emit([]);\n return r;\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** Picks the mixed colour on the pad (averaged over `radius` px); with `setColor` it becomes the paint colour. */\n mixingPadPick(x: number, y: number, options: { radius?: number; setColor?: boolean } = {}): string | null {\n let c: string | null;\n try {\n c = this.pad.pick(x, y, options.radius);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n if (c && options.setColor) this.setColor(c);\n return c;\n }\n\n clearMixingPad(): void {\n this.pad.clear();\n this.emit([]);\n }\n\n /**\n * Mixes paints on the pad like a painter: blobs in proportion to their `parts`, knife passes,\n * and a fully mixed core of the Kubelka-Munk mix (see `MixingPad.mix`). With `setColor` the\n * mix becomes the paint colour. Returns the mixed colour and the pile's bounds.\n */\n mixOnPad(paints: readonly { color: string; parts: number }[], options: PadMixOptions & { setColor?: boolean } = {}): { color: string; bounds: RasterRect } {\n const parsed = (paints ?? []).map((p, i) => ({ color: parseColor(p?.color, `paints[${i}].color`), parts: p?.parts }));\n let r: { color: string | null; bounds: RasterRect };\n try {\n r = this.pad.mix(parsed, options);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n const color = r.color ?? parsed[0].color;\n this.emit([]);\n if (options.setColor) this.setColor(color);\n return { color, bounds: r.bounds };\n }\n\n /** The mixing pad as a PNG data URL (with alpha). */\n mixingPadPng(): string {\n return this.pad.toPng();\n }\n\n /** Replaces the mixing pad with a PNG (data URL or base64, 360 × 220). */\n setMixingPadPng(png: string): void {\n try {\n this.pad.fromPng(png);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.emit([]);\n }\n\n /** The mixing pads kept by name. */\n listMixingPads(): SavedMixingPadInfo[] {\n return this.savedPads.map(({ id, name }) => ({ id, name }));\n }\n\n /** Keeps the mixing pad as it is now under `name` (saved with the document). Returns its id. */\n saveMixingPad(name?: string): SavedMixingPadInfo {\n if (this.pad.empty) throw new PaintEditorError('The mixing pad is empty; put paint on it (mixing_pad_stroke or mix_on_pad) before saving it.');\n const label = (typeof name === 'string' && name.trim() ? name.trim() : `Pad ${this.nextPadId}`).slice(0, 80);\n const pad: SavedMixingPad = { id: `pad${this.nextPadId++}`, name: label, png: this.pad.toPng() };\n this.savedPads.push(pad);\n this.emit([]);\n return { id: pad.id, name: pad.name };\n }\n\n private requirePad(id: string): SavedMixingPad {\n const p = this.savedPads.find((x) => x.id === id);\n if (!p) throw new PaintEditorError(`Unknown mixing pad \"${id}\". Call list_mixing_pads for the ids.`);\n return p;\n }\n\n /** Puts a kept pad back on the mixing pad (replacing what is there). */\n loadMixingPad(id: string): SavedMixingPadInfo {\n const p = this.requirePad(id);\n this.setMixingPadPng(p.png);\n return { id: p.id, name: p.name };\n }\n\n /** Forgets a kept pad. */\n removeMixingPad(id: string): SavedMixingPadInfo {\n const p = this.requirePad(id);\n this.savedPads = this.savedPads.filter((x) => x !== p);\n this.emit([]);\n return { id: p.id, name: p.name };\n }\n\n private mixingPadJSON(): { current?: string; saved: SavedMixingPad[] } | undefined {\n if (this.pad.empty && !this.savedPads.length) return undefined;\n return { ...(this.pad.empty ? {} : { current: this.pad.toPng() }), saved: this.savedPads.map((p) => ({ ...p })) };\n }\n\n private readMixingPad(raw: unknown, problems: string[]): void {\n this.savedPads = [];\n this.nextPadId = 1;\n this.pad.clear();\n if (raw === undefined || raw === null) return;\n const r = raw as { current?: unknown; saved?: unknown };\n if (typeof r.current === 'string') {\n try {\n this.pad.fromPng(r.current);\n } catch (e) {\n problems.push(`The mixing pad could not be read (${(e as Error).message}); it starts empty.`);\n }\n }\n if (Array.isArray(r.saved)) {\n r.saved.forEach((p, i) => {\n const x = p as Partial<SavedMixingPad>;\n if (!x || typeof x.png !== 'string' || typeof x.name !== 'string') {\n problems.push(`mixingPad.saved[${i}] is not a saved pad; skipped.`);\n return;\n }\n const id = typeof x.id === 'string' && /^pad\\d+$/.test(x.id) ? x.id : `pad${this.nextPadId}`;\n const n = Number(id.slice(3));\n if (n >= this.nextPadId) this.nextPadId = n + 1;\n this.savedPads.push({ id, name: x.name, png: x.png });\n });\n }\n }\n\n /** A layer's tile surface (live while stroking), for thumbnails. */\n layerSurface(id: string): TiledSurface | null {\n const l = findLayer(this.currentLayers(), id)?.layer;\n return isPixelLayer(l) ? l.surface : null;\n }\n}\n\nexport type { Tile };\n\n/** Largest tracing or reference image side, in pixels. */\nexport const MAX_IMAGE_SIDE = 8192;\n\n/** `src` drawn into `rect` of a `width × height` canvas (bilinear), canvas-sized RGBA. */\nfunction resampleInto(src: PixelImage, rect: { x: number; y: number; width: number; height: number }, width: number, height: number): PixelImage {\n const out = new Uint8ClampedArray(width * height * 4);\n const x0 = Math.max(0, Math.floor(rect.x));\n const y0 = Math.max(0, Math.floor(rect.y));\n const x1 = Math.min(width, Math.ceil(rect.x + rect.width));\n const y1 = Math.min(height, Math.ceil(rect.y + rect.height));\n const sx = src.width / rect.width;\n const sy = src.height / rect.height;\n const d = src.data;\n for (let y = y0; y < y1; y++) {\n const v = Math.min(src.height - 1, Math.max(0, (y + 0.5 - rect.y) * sy - 0.5));\n const j0 = Math.floor(v);\n const j1 = Math.min(src.height - 1, j0 + 1);\n const fy = v - j0;\n for (let x = x0; x < x1; x++) {\n const u = (x + 0.5 - rect.x) * sx - 0.5;\n if (u < -0.5 || u > src.width - 0.5 || (y + 0.5 - rect.y) * sy < 0 || (y + 0.5 - rect.y) * sy > src.height) continue;\n const uc = Math.min(src.width - 1, Math.max(0, u));\n const i0 = Math.floor(uc);\n const i1 = Math.min(src.width - 1, i0 + 1);\n const fx = uc - i0;\n const o = (y * width + x) * 4;\n for (let c = 0; c < 4; c++) {\n const a = d[(j0 * src.width + i0) * 4 + c] + (d[(j0 * src.width + i1) * 4 + c] - d[(j0 * src.width + i0) * 4 + c]) * fx;\n const b = d[(j1 * src.width + i0) * 4 + c] + (d[(j1 * src.width + i1) * 4 + c] - d[(j1 * src.width + i0) * 4 + c]) * fx;\n out[o + c] = a + (b - a) * fy;\n }\n }\n }\n return { width, height, data: out };\n}\n","// Agent \"performances\" on the paint canvas, after the chart and diagram directors (see\n// `animation/presence.ts`): a director acts each command out with the agent's cursor in document\n// pixels, so a stroke is painted live along its path (the canvas updates as the dabs land) and\n// commits as one undo step, a fill is rubbed across the sheet, and a replay starts from the strokes\n// it copies. The clock is injectable so performances are repeatable in tests.\nimport {\n PerformanceQueue,\n PresenceStore,\n realPresenceClock,\n tweenPresenceCursor,\n type PresenceActor,\n type PresenceActorInfo,\n type PresenceClock,\n type PresenceState,\n} from '../animation/presence';\nimport type { RasterRect } from '../raster/surface';\nimport type { FillLayerOptions, PaintEditorController, PaintStrokeInit, ReplayOptions } from './controller';\nimport type { PenPoint } from './engine';\nimport type { PaintStrokeResult } from './stroke';\n\nexport type PaintPresenceActor = PresenceActor;\nexport type PaintPresenceState = PresenceState;\n\n/** Cursor positions (document pixels) and notes of agents painting, for an overlay. */\nexport class PaintPresence extends PresenceStore {}\n\n/** What an agent asks for. Each command that paints commits as one undo step. */\nexport type PaintAgentCommand =\n | { kind: 'stroke'; stroke: PaintStrokeInit & { points: readonly PenPoint[] }; note?: string }\n | { kind: 'fill'; fill: FillLayerOptions; note?: string }\n | { kind: 'replay'; replay: ReplayOptions; note?: string }\n | { kind: 'point'; at: { x: number; y: number }; note?: string }\n | { kind: 'say'; text: string };\n\nexport type PaintPerformanceResult =\n | { ok: true; command: PaintAgentCommand; result?: unknown }\n | { ok: false; command: PaintAgentCommand; message: string };\n\nexport interface PaintDirectorOptions {\n clock?: PresenceClock;\n /** 1 = natural speed; 2 = twice as fast. */\n speed?: number;\n /** Hide each actor's cursor this long after its last command (ms, default 1500; 0 keeps it). */\n linger?: number;\n /** Longest a stroke takes to paint at speed 1 (ms, default 1800); longer strokes are sped up. */\n maxStrokeMs?: number;\n}\n\nconst FRAME = 16;\n\n/** Where along a stroke each point is reached, 0..1: by its time `t` when the points have times, else by distance. */\nexport function strokeSchedule(points: readonly PenPoint[]): number[] {\n if (points.length < 2) return points.map(() => 1);\n const t0 = points[0]!.t;\n const t1 = points[points.length - 1]!.t;\n if (points.every((p) => Number.isFinite(p.t)) && (t1 as number) > (t0 as number)) {\n return points.map((p) => Math.min(1, Math.max(0, ((p.t as number) - (t0 as number)) / ((t1 as number) - (t0 as number)))));\n }\n const acc = [0];\n for (let i = 1; i < points.length; i++) acc.push(acc[i - 1]! + Math.hypot(points[i]!.x - points[i - 1]!.x, points[i]!.y - points[i - 1]!.y));\n const total = acc[acc.length - 1]!;\n return total > 0 ? acc.map((d) => d / total) : points.map(() => 1);\n}\n\n/** How long a stroke takes to paint at speed 1 (ms): its recorded duration, else ~1.5 px/ms, between 250 ms and `max`. */\nexport function strokeDuration(points: readonly PenPoint[], max = 1800): number {\n const t0 = points[0]?.t;\n const t1 = points[points.length - 1]?.t;\n let ms: number;\n if (points.every((p) => Number.isFinite(p.t)) && (t1 as number) > (t0 as number)) ms = (t1 as number) - (t0 as number);\n else {\n let length = 0;\n for (let i = 1; i < points.length; i++) length += Math.hypot(points[i]!.x - points[i - 1]!.x, points[i]!.y - points[i - 1]!.y);\n ms = length / 1.5;\n }\n return Math.min(max, Math.max(250, ms));\n}\n\n/** A zig-zag across `rect` (for a fill rubbed over the sheet), `rows` passes from the top. */\nexport function sweepPath(rect: RasterRect, rows = 4): Array<{ x: number; y: number }> {\n const inset = Math.min(rect.width, rect.height) * 0.08;\n const left = rect.x + inset;\n const right = rect.x + rect.width - inset;\n const out: Array<{ x: number; y: number }> = [];\n for (let r = 0; r < rows; r++) {\n const y = rect.y + inset + ((rect.height - 2 * inset) * (r + 0.5)) / rows;\n out.push(r % 2 ? { x: right, y } : { x: left, y }, r % 2 ? { x: left, y } : { x: right, y });\n }\n return out;\n}\n\nconst centre = (r: RasterRect) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });\n\n/**\n * Acts out agent commands on a paint canvas: the actor's cursor glides to the stroke's start and\n * paints it live along its points (paced by their times, or evenly), the stroke committing as\n * one undo step; fills are rubbed across the sheet first; replays start from the strokes they\n * copy and end on what they painted. Commands queue per actor. A command that cannot apply (an\n * unknown or locked layer, a stroke the user is still painting) fails with the controller's\n * sentence before anything is painted.\n */\nexport class PaintDirector {\n private readonly clock: PresenceClock;\n private readonly queue: PerformanceQueue;\n\n constructor(\n private readonly controller: PaintEditorController,\n readonly presence: PaintPresence,\n private readonly options: PaintDirectorOptions = {}\n ) {\n this.clock = options.clock ?? realPresenceClock;\n this.queue = new PerformanceQueue(presence, { clock: this.clock, linger: options.linger });\n }\n\n /** Acts out `commands` in order for `actor`. Resolves with one result per command. */\n perform(actor: PresenceActorInfo, commands: readonly PaintAgentCommand[]): Promise<PaintPerformanceResult[]> {\n return this.queue.run(actor, async () => {\n const results: PaintPerformanceResult[] = [];\n for (const command of commands) {\n try {\n results.push({ ok: true, command, result: await this.performOne(actor.id, command) });\n } catch (error) {\n results.push({ ok: false, command, message: error instanceof Error ? error.message : String(error) });\n }\n }\n return results;\n });\n }\n\n private ms(n: number): number {\n return Math.round(n / Math.max(0.1, this.options.speed ?? 1));\n }\n\n private where(id: string) {\n const a = this.presence.get(id);\n return { x: a?.x ?? 0, y: a?.y ?? 0 };\n }\n\n /** Glides to `to`; an actor that just appeared starts there instead of the corner. */\n private async glide(id: string, to: { x: number; y: number }, ms = 380) {\n const a = this.presence.get(id);\n if (!a || (a.x === 0 && a.y === 0)) {\n this.presence.set({ id, x: to.x, y: to.y });\n return;\n }\n await tweenPresenceCursor(this.presence, this.clock, id, this.where(id), to, this.ms(ms));\n }\n\n private async say(id: string, text: string | undefined, ms: number) {\n if (!text) return;\n this.presence.set({ id, message: text });\n await this.clock.wait(this.ms(ms));\n }\n\n private async performOne(id: string, command: PaintAgentCommand): Promise<unknown> {\n const c = this.controller;\n switch (command.kind) {\n case 'say':\n await this.say(id, command.text, 900);\n return undefined;\n case 'point':\n await this.say(id, command.note, 500);\n await this.glide(id, command.at);\n this.presence.set({ id, pressed: true });\n await this.clock.wait(this.ms(110));\n this.presence.set({ id, pressed: false });\n return undefined;\n case 'stroke':\n await this.say(id, command.note, 500);\n return this.stroke(id, command.stroke);\n case 'fill': {\n await this.say(id, command.note, 500);\n const { width, height } = c.getState().document.canvas;\n const path = sweepPath({ x: 0, y: 0, width, height });\n await this.glide(id, path[0]!);\n this.presence.set({ id, pressed: true });\n try {\n for (let i = 1; i < path.length; i++) await tweenPresenceCursor(this.presence, this.clock, id, path[i - 1]!, path[i]!, this.ms(i % 2 ? 260 : 90));\n } finally {\n this.presence.set({ id, pressed: false });\n }\n return c.fillLayer(command.fill);\n }\n case 'replay': {\n await this.say(id, command.note, 500);\n const source = c.getStrokes().filter((s) => command.replay.strokeIds?.includes(s.id));\n const first = source[0]?.points[0];\n if (first) await this.glide(id, first);\n const result = c.replayStrokes(command.replay);\n if (result.bounds) await this.glide(id, centre(result.bounds), 520);\n return result;\n }\n }\n }\n\n /** Paints a stroke live: begins it, pushes its points frame by frame as the cursor reaches them, then ends it (one undo step). */\n private async stroke(id: string, init: PaintStrokeInit & { points: readonly PenPoint[] }): Promise<PaintStrokeResult> {\n const c = this.controller;\n const points = c.checkPoints(init.points);\n const { points: _points, ...rest } = init;\n // Validate the layer and brush (and that nothing else is painting) before moving at all.\n const session = c.beginStroke({ snapToPerspective: false, start: points[0], ...rest });\n try {\n await this.glide(id, points[0]!);\n this.presence.set({ id, pressed: true });\n const at = strokeSchedule(points);\n const duration = this.ms(strokeDuration(points, this.options.maxStrokeMs));\n let next = 0;\n const frames = Math.max(1, Math.round(duration / FRAME));\n for (let f = 1; f <= frames; f++) {\n const t = f / frames;\n const from = next;\n while (next < points.length && at[next]! <= t + 1e-9) next++;\n if (next > from) {\n session.push(points.slice(from, next));\n const p = points[next - 1]!;\n this.presence.set({ id, x: p.x, y: p.y });\n }\n if (f < frames) await this.clock.wait(duration / frames);\n }\n if (next < points.length) session.push(points.slice(next));\n this.presence.set({ id, pressed: false });\n return session.end();\n } catch (error) {\n if (c.activeStroke === session) session.cancel();\n this.presence.set({ id, pressed: false });\n throw error;\n }\n }\n}\n","/**\n * Agent tools for stickers: stamps sprayed with a sticker brush (`paint_stroke`, list_brushes\n * category sticker) stay objects on their layer, which these tools list, move, turn, resize,\n * recolour, reorder and delete. Each change is one undo step and redraws the layer.\n */\n\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\nimport { STAMP_KINDS } from './stickers';\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;\nconst LAYER_ID = { type: 'string', description: 'Sticker layer id from list_layers (default the active layer).' } as const;\n\nexport function createPaintStickerTools(c: PaintEditorController): AgentTool[] {\n return [\n {\n name: 'list_stickers',\n title: 'List stickers',\n description: 'Lists a layer’s stickers bottom first: id, stamp, x, y (centre, document pixels), size (diameter px), rotation (degrees clockwise), color, opacity. Empty for a layer without stickers. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'List stickers', ...READ },\n handler: ({ layerId }) => ({ layerId: layerId ?? c.document.activeLayerId, stickers: c.listStickers(layerId) }),\n },\n {\n name: 'update_sticker',\n title: 'Change a sticker',\n description: 'Moves, turns, resizes, recolours, fades or restamps one sticker by id from list_stickers. One undo step. Returns the sticker.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n id: { type: 'string', description: 'Sticker id from list_stickers.' },\n stamp: { type: 'string', enum: [...STAMP_KINDS], description: 'Another stamp.' },\n x: { type: 'number', description: 'Centre x in document pixels.' },\n y: { type: 'number', description: 'Centre y in document pixels.' },\n size: { type: 'number', minimum: 2, maximum: 2048, description: 'Diameter in pixels.' },\n rotation: { type: 'number', minimum: -3600, maximum: 3600, description: 'Degrees clockwise.' },\n color: { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'Colour #rrggbb.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a sticker', ...WRITE },\n handler: ({ layerId, id, ...update }) => c.updateSticker(layerId, id, update),\n },\n {\n name: 'arrange_stickers',\n title: 'Bring stickers forward or back',\n description: 'Moves stickers to the top (front) or bottom (back) of their layer, keeping their order among themselves. One undo step. Returns the new order of ids.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n ids: { type: 'array', minItems: 1, items: { type: 'string', description: 'Sticker id.' }, description: 'Sticker ids from list_stickers.' },\n to: { type: 'string', enum: ['front', 'back'], description: 'front: on top of the others; back: under them.' },\n },\n required: ['ids', 'to'],\n additionalProperties: false,\n },\n annotations: { title: 'Bring stickers forward or back', ...WRITE },\n handler: ({ layerId, ids, to }) => ({ order: c.arrangeStickers(layerId, ids, to) }),\n },\n {\n name: 'remove_stickers',\n title: 'Delete stickers',\n description: 'Deletes stickers by id, or every sticker on the layer when ids is missing. One undo step. Returns the ids removed.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, ids: { type: 'array', items: { type: 'string', description: 'Sticker id.' }, description: 'Sticker ids (default all).' } }, additionalProperties: false },\n annotations: { title: 'Delete stickers', ...DESTRUCTIVE },\n handler: ({ layerId, ids }) => ({ removed: c.removeStickers(layerId, ids) }),\n },\n ];\n}\n","/**\n * Agent tools for the paint editor's painting guides (symmetry, perspective, tracing and reference\n * images) and colour (harmonies, swatches, the mixing pad). Guides, swatches and the pad are\n * editor state saved with the document (the pad is not saved), not undo steps.\n */\n\nimport { PAINT_SOURCE_JSON_SCHEMA } from '../paint/gradient-paint';\nimport type { AgentTool } from '../tools/registry.js';\nimport { bytesToBase64, encodePng } from '../raster/png';\nimport { HARMONY_KINDS } from './color';\nimport type { PaintEditorController } from './controller';\nimport { SYMMETRY_MODES, symmetryCopies, type ReferenceImage, type TracingImage } from './guides';\nimport type { PenPoint } from './engine';\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 SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\nconst EMPTY = { type: 'object', properties: {}, additionalProperties: false } as const;\nconst COLOR = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'Colour #rrggbb.' } as const;\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'x in document pixels.' }, y: { type: 'number', description: 'y in document pixels.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\nconst RECT = {\n type: 'object',\n description: 'Placement in document pixels.',\n properties: {\n x: { type: 'number', description: 'Left edge.' },\n y: { type: 'number', description: 'Top edge.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height.' },\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false,\n} as const;\nconst PNG = { type: 'string', minLength: 1, description: 'The image as a PNG, base64 or a data:image/png URL.' } as const;\n\nconst tracingInfo = (t: TracingImage | null) => (t ? { width: t.width, height: t.height, rect: t.rect, opacity: t.opacity, visible: t.visible, sampleColors: t.sampleColors } : null);\nconst refInfo = ({ src: _s, ...r }: ReferenceImage) => r;\n\nexport function createPaintGuideTools(c: PaintEditorController): AgentTool[] {\n const guides = () => c.getState().guides;\n const STENCIL_GEOMETRY = {\n mode: { type: 'string', enum: ['guide', 'mask'], description: 'guide: strokes that start within snap px of the edge follow it; mask: paint lands only inside (or on the left of a line going from → to), outside with invert. Default guide for ruler and arc, mask for line and shape.' },\n enabled: { type: 'boolean', description: 'In effect and shown (default true).' },\n from: { ...POINT, description: 'ruler / line: a point on the edge.' },\n to: { ...POINT, description: 'ruler / line: another point on the edge (the edge runs on past both).' },\n center: { ...POINT, description: 'arc: the centre.' },\n radius: { type: 'number', exclusiveMinimum: 0, description: 'arc: the radius in pixels.' },\n startAngle: { type: 'number', minimum: -720, maximum: 720, description: 'arc: where it starts, degrees clockwise from +x (default 0).' },\n endAngle: { type: 'number', minimum: -720, maximum: 720, description: 'arc: where it ends (default 360: the whole circle).' },\n shape: { type: 'string', enum: ['rect', 'ellipse', 'polygon'], description: 'shape: rect or ellipse (with rect) or polygon (with points).' },\n rect: { ...RECT, description: 'shape rect / ellipse: its bounds in document pixels.' },\n points: { type: 'array', minItems: 3, maxItems: 500, items: POINT, description: 'shape polygon: its corners in order.' },\n invert: { type: 'boolean', description: 'mask: paint outside the shape (or on the right of the line) instead (default false).' },\n snap: { type: 'number', minimum: 1, maximum: 400, description: 'guide: how near the edge a stroke must start to follow it, px (default 40).' },\n feather: { type: 'number', minimum: 0, maximum: 100, description: 'mask: soft edge width in pixels (default 1).' },\n } as const;\n return [\n {\n name: 'get_guides',\n title: 'Get the painting guides',\n description: 'Reads the painting guides: symmetry (mode, centre, rotation, segments, mirror, and how many copies each stroke paints), the perspective guide (vanishing points, snap), the tracing image (size, placement, opacity, visibility, colour sampling), the reference images and the stencils (id, kind, mode, geometry).',\n inputSchema: EMPTY,\n annotations: { title: 'Get the painting guides', ...READ },\n handler: () => {\n const g = guides();\n return { symmetry: { ...g.symmetry, copies: symmetryCopies(g.symmetry).length }, perspective: g.perspective, tracing: tracingInfo(g.tracing), references: g.references.map(refInfo), stencils: g.stencils };\n },\n },\n {\n name: 'set_symmetry',\n title: 'Set mirror or kaleidoscope painting',\n description:\n 'Turns symmetry on or off. Every stroke after it also paints its copies, as one stroke and one undo step: vertical, horizontal or diagonal mirror (one copy), dual (four-way), or kaleidoscope with n segments (rotated copies, and with mirror the reflections between them). Editor state, saved with the document. Returns the guide and the number of copies.',\n inputSchema: {\n type: 'object',\n properties: {\n mode: { type: 'string', enum: [...SYMMETRY_MODES], description: 'off, vertical (mirror left / right), horizontal (top / bottom), dual, diagonal or kaleidoscope.' },\n center: { ...POINT, description: 'Centre of the axes in document pixels (default the canvas centre).' },\n rotation: { type: 'number', minimum: -360, maximum: 360, description: 'Turn the axes, degrees clockwise.' },\n segments: { type: 'integer', minimum: 2, maximum: 32, description: 'Kaleidoscope segments (default 6).' },\n mirror: { type: 'boolean', description: 'Kaleidoscope: also reflect alternate segments (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set mirror or kaleidoscope painting', ...SET },\n handler: (a) => {\n const s = c.setSymmetry(a);\n return { ...s, copies: symmetryCopies(s).length };\n },\n },\n {\n name: 'set_perspective_guide',\n title: 'Set the perspective guide',\n description:\n 'Sets one to three vanishing points (document pixels, may be off the canvas). With snap on, pen strokes snap to the line through their start towards a vanishing point (one-point also keeps horizontals and verticals, two-point verticals); for paint_stroke pass snapToPerspective. Editor state, saved with the document. Returns the guide.',\n inputSchema: {\n type: 'object',\n properties: {\n enabled: { type: 'boolean', description: 'Show and use the guide.' },\n points: { type: 'array', minItems: 1, maxItems: 3, items: { ...POINT, description: 'A vanishing point.' }, description: 'The vanishing points.' },\n snap: { type: 'boolean', description: 'Snap strokes to the guide lines (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the perspective guide', ...SET },\n handler: (a) => c.setPerspective(a),\n },\n {\n name: 'set_tracing_image',\n title: 'Set a tracing image',\n description:\n 'Puts an image under the paint to trace over (view only: never exported), fitted to the canvas unless rect is given, at opacity (default 0.35). With sampleColors, strokes without a colour take the image colour where they start, so painting over it copies its colours. Replaces any tracing image. Returns its size and placement.',\n inputSchema: {\n type: 'object',\n properties: {\n png: PNG,\n rect: RECT,\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1 (default 0.35).' },\n sampleColors: { type: 'boolean', description: 'Strokes take their colour from the image (default false).' },\n },\n required: ['png'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a tracing image', ...WRITE },\n handler: (a) => tracingInfo(c.setTracingImage(a)),\n },\n {\n name: 'update_tracing_image',\n title: 'Change the tracing image',\n description: 'Changes the tracing image opacity, visibility, placement or colour sampling. Returns it.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1.' },\n visible: { type: 'boolean', description: 'Show or hide it.' },\n sampleColors: { type: 'boolean', description: 'Strokes take their colour from it.' },\n rect: RECT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Change the tracing image', ...SET },\n handler: (a) => tracingInfo(c.updateTracing(a)),\n },\n {\n name: 'clear_tracing_image',\n title: 'Remove the tracing image',\n description: 'Removes the tracing image. Returns null.',\n inputSchema: EMPTY,\n annotations: { title: 'Remove the tracing image', ...DESTRUCTIVE },\n handler: () => {\n c.clearTracingImage();\n return { tracing: null };\n },\n },\n {\n name: 'add_reference_image',\n title: 'Add a reference image',\n description: 'Adds a floating reference image (shown beside the canvas, never painted) to look at and pick colours from with pick_reference_color. Returns its id, size and placement.',\n inputSchema: {\n type: 'object',\n properties: {\n png: PNG,\n name: { type: 'string', minLength: 1, description: 'Display name.' },\n x: { type: 'number', description: 'Panel left in viewport pixels.' },\n y: { type: 'number', description: 'Panel top in viewport pixels.' },\n displayWidth: { type: 'number', minimum: 16, maximum: 4096, description: 'Panel width in screen pixels (default up to 240).' },\n },\n required: ['png'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a reference image', ...WRITE },\n handler: (a) => refInfo(c.addReferenceImage(a)),\n },\n {\n name: 'list_reference_images',\n title: 'List reference images',\n description: 'Lists the reference images: id, name, size in pixels, panel placement and visibility.',\n inputSchema: EMPTY,\n annotations: { title: 'List reference images', ...READ },\n handler: () => ({ references: guides().references.map(refInfo) }),\n },\n {\n name: 'update_reference_image',\n title: 'Change a reference image',\n description: 'Moves, resizes, renames, shows or hides a reference image panel. Returns it.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Reference id from list_reference_images.' },\n name: { type: 'string', minLength: 1, description: 'Display name.' },\n x: { type: 'number', description: 'Panel left in viewport pixels.' },\n y: { type: 'number', description: 'Panel top in viewport pixels.' },\n displayWidth: { type: 'number', minimum: 16, maximum: 4096, description: 'Panel width in screen pixels.' },\n visible: { type: 'boolean', description: 'Show or hide it.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a reference image', ...SET },\n handler: ({ id, ...u }) => refInfo(c.updateReferenceImage(id, u)),\n },\n {\n name: 'remove_reference_image',\n title: 'Remove a reference image',\n description: 'Removes a reference image. Returns the removed id.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Reference id from list_reference_images.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Remove a reference image', ...DESTRUCTIVE },\n handler: ({ id }) => {\n c.removeReferenceImage(id);\n return { removed: id };\n },\n },\n {\n name: 'pick_reference_color',\n title: 'Pick a colour from a reference',\n description: 'Reads the colour of a reference image at its own pixel (x, y). With setColor it becomes the paint colour. Returns #rrggbb.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Reference id from list_reference_images.' },\n x: { type: 'number', minimum: 0, description: 'x in the image’s pixels.' },\n y: { type: 'number', minimum: 0, description: 'y in the image’s pixels.' },\n setColor: { type: 'boolean', description: 'Also make it the paint colour (default false).' },\n },\n required: ['id', 'x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Pick a colour from a reference', ...SET },\n handler: ({ id, x, y, setColor }) => {\n const color = c.pickReferenceColor(id, x, y);\n if (setColor) c.setColor(color);\n return { color };\n },\n },\n {\n name: 'color_harmony',\n title: 'Colour harmonies',\n description:\n 'Returns the colours of a harmony around a colour (default the paint colour): complementary, split-complementary, analogous, triadic, tetradic, square or monochromatic, on the painter’s red-yellow-blue wheel (default: red’s complement is green) or the RGB wheel. Changes nothing. Returns the colours with their wheel angles, the base first.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: [...HARMONY_KINDS], description: 'The harmony.' },\n color: { ...COLOR, description: 'Base colour #rrggbb (default the paint colour).' },\n wheel: { type: 'string', enum: ['ryb', 'rgb'], description: 'ryb (painter’s wheel, default) or rgb.' },\n spread: { type: 'number', minimum: 5, maximum: 90, description: 'Analogous and split-complementary spread in degrees (default 30).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Colour harmonies', ...READ },\n handler: ({ kind, color, wheel, spread }) => ({ kind, wheel: wheel ?? 'ryb', colors: c.harmony(kind, { color, wheel, spread }) }),\n },\n {\n name: 'list_swatches',\n title: 'List swatches',\n description: 'Lists the swatches (index, colour, name, and paint for a gradient swatch).',\n inputSchema: EMPTY,\n annotations: { title: 'List swatches', ...READ },\n handler: () => ({ swatches: c.getState().swatches.map((s, index) => ({ index, ...s })) }),\n },\n {\n name: 'add_swatch',\n title: 'Add a swatch',\n description: 'Adds a swatch (saved with the document): a colour, or with paint a gradient (set_paint picks it up again from list_swatches). Give color or paint. Returns the swatches; a gradient swatch has paint and its first colour as color.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { ...COLOR, description: 'A colour #rrggbb.' },\n paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: 'Gradient paint (or a colour) for the swatch, instead of color.' },\n name: { type: 'string', minLength: 1, description: 'Optional name.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a swatch', ...WRITE },\n handler: ({ color, paint, name }) => {\n if ((color === undefined) === (paint === undefined)) throw new Error('Give color (#rrggbb) or paint (a colour or gradient), not both and not neither.');\n return { swatches: c.addSwatch(paint ?? color, name).map((s, index) => ({ index, ...s })) };\n },\n },\n {\n name: 'remove_swatch',\n title: 'Remove a swatch',\n description: 'Removes a swatch by index (from list_swatches). Returns the swatches.',\n inputSchema: { type: 'object', properties: { index: { type: 'integer', minimum: 0, description: '0-based index.' } }, required: ['index'], additionalProperties: false },\n annotations: { title: 'Remove a swatch', ...DESTRUCTIVE },\n handler: ({ index }) => ({ swatches: c.removeSwatch(index).map((s, i) => ({ index: i, ...s })) }),\n },\n {\n name: 'mixing_pad_stroke',\n title: 'Paint on the mixing pad',\n description:\n 'Paints on the mixing pad (a 360 × 220 scratch canvas, not the painting): tool paint lays down a colour with a pigment Mixer Brush; tool knife smears what is there like a palette knife, mixing it as paint. Points are pad pixels. Not an undo step. Returns the changed bounds; then pick_mixing_pad_color. For a measured mix in one call use mix_on_pad.',\n inputSchema: {\n type: 'object',\n properties: {\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 2000,\n description: 'The path in pad pixels (0..360, 0..220).',\n items: {\n type: 'object',\n properties: { x: { type: 'number', description: 'Pad x.' }, y: { type: 'number', description: 'Pad y.' }, pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pressure (default 1).' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n tool: { type: 'string', enum: ['paint', 'knife'], description: 'paint (default) or knife.' },\n color: { ...COLOR, description: 'Paint colour (default the paint colour; ignored by the knife).' },\n size: { type: 'number', minimum: 1, maximum: 200, description: 'Tool size in pad pixels (default 28 paint, 34 knife).' },\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint on the mixing pad', ...WRITE },\n handler: ({ points, tool, color, size }) => c.mixingPadStroke(points as PenPoint[], { tool, color, size }),\n },\n {\n name: 'pick_mixing_pad_color',\n title: 'Pick from the mixing pad',\n description: 'Reads the mixed colour on the pad at (x, y), averaged over radius pixels (default 3); with setColor it becomes the paint colour. Returns #rrggbb, or null where the pad is bare.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, maximum: 360, description: 'Pad x.' },\n y: { type: 'number', minimum: 0, maximum: 220, description: 'Pad y.' },\n radius: { type: 'integer', minimum: 0, maximum: 20, description: 'Averaging radius in pixels (default 3).' },\n setColor: { type: 'boolean', description: 'Also make it the paint colour (default false).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Pick from the mixing pad', ...SET },\n handler: ({ x, y, radius, setColor }) => ({ color: c.mixingPadPick(x, y, { radius, setColor }) }),\n },\n {\n name: 'clear_mixing_pad',\n title: 'Clear the mixing pad',\n description: 'Wipes the mixing pad clean.',\n inputSchema: EMPTY,\n annotations: { title: 'Clear the mixing pad', ...DESTRUCTIVE },\n handler: () => {\n c.clearMixingPad();\n return { cleared: true };\n },\n },\n {\n name: 'mix_on_pad',\n title: 'Mix paints on the pad',\n description:\n 'Mixes paints on the mixing pad as a painter would: blobs whose areas follow their parts, worked with the palette knife, and a fully mixed core of the Kubelka-Munk (pigment) mix, so blue and yellow make green and white tints. One call per mix. Not an undo step (the pad is scratch space). Returns the mixed colour #rrggbb and the pile bounds in pad pixels; with setColor it becomes the paint colour. pick_mixing_pad_color reads the streaks around it.',\n inputSchema: {\n type: 'object',\n properties: {\n paints: {\n type: 'array',\n minItems: 1,\n maxItems: 8,\n description: 'The paints, 1 to 8.',\n items: {\n type: 'object',\n properties: {\n color: { ...COLOR, description: 'Paint colour #rrggbb.' },\n parts: { type: 'number', minimum: 0, description: 'Parts of this paint, any scale (1 and 3 for a 1:3 mix); at least one above 0.' },\n },\n required: ['color', 'parts'],\n additionalProperties: false,\n },\n },\n x: { type: 'number', minimum: 0, maximum: 360, description: 'Centre of the pile, pad x (default 180).' },\n y: { type: 'number', minimum: 0, maximum: 220, description: 'Centre of the pile, pad y (default 110).' },\n radius: { type: 'number', minimum: 12, maximum: 100, description: 'Radius of the pile in pad pixels (default 40); it must fit on the pad.' },\n passes: { type: 'integer', minimum: 1, maximum: 20, description: 'Palette-knife passes (default 3; more gives finer streaks around the core).' },\n clear: { type: 'boolean', description: \"Wipe the pile's area first (default true); false mixes into the paint already there.\" },\n setColor: { type: 'boolean', description: 'Also make the mix the paint colour (default false).' },\n },\n required: ['paints'],\n additionalProperties: false,\n },\n annotations: { title: 'Mix paints on the pad', ...WRITE },\n handler: ({ paints, x, y, radius, passes, clear, setColor }) => c.mixOnPad(paints, { x, y, radius, passes, clear, setColor }),\n },\n {\n name: 'save_mixing_pad',\n title: 'Keep the mixing pad',\n description: 'Keeps the mixing pad as it is now under a name, saved with the document, to come back to with load_mixing_pad. Refused while the pad is empty. Returns the kept pad id and name.',\n inputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 80, description: 'Name (default \"Pad n\").' } }, additionalProperties: false },\n annotations: { title: 'Keep the mixing pad', ...WRITE },\n handler: ({ name }) => c.saveMixingPad(name),\n },\n {\n name: 'list_mixing_pads',\n title: 'List kept mixing pads',\n description: 'Lists the mixing pads kept with save_mixing_pad (id, name). Changes nothing.',\n inputSchema: EMPTY,\n annotations: { title: 'List kept mixing pads', ...READ },\n handler: () => ({ pads: c.listMixingPads() }),\n },\n {\n name: 'load_mixing_pad',\n title: 'Load a kept mixing pad',\n description: 'Puts a kept pad (id from list_mixing_pads) back on the mixing pad, replacing what is on it. Returns the pad id and name.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Kept pad id from list_mixing_pads.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Load a kept mixing pad', ...DESTRUCTIVE },\n handler: ({ id }) => c.loadMixingPad(id),\n },\n {\n name: 'remove_mixing_pad',\n title: 'Remove a kept mixing pad',\n description: 'Forgets a kept pad (id from list_mixing_pads); the pad on screen is untouched. Returns the removed id and name.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Kept pad id from list_mixing_pads.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Remove a kept mixing pad', ...DESTRUCTIVE },\n handler: ({ id }) => c.removeMixingPad(id),\n },\n {\n name: 'render_mixing_pad',\n title: 'Render the mixing pad',\n description: 'Renders the mixing pad as a PNG (360 × 220, over white). Changes nothing.',\n inputSchema: EMPTY,\n annotations: { title: 'Render the mixing pad', ...READ },\n handler: () => {\n const pad = c.mixingPad;\n return {\n content: [\n { type: 'image', data: bytesToBase64(encodePng(pad.width, pad.height, pad.pixels())), mimeType: 'image/png' },\n { type: 'text', text: `Mixing pad ${pad.width}×${pad.height}` },\n ],\n };\n },\n },\n {\n name: 'add_stencil',\n title: 'Lay a stencil',\n description:\n 'Lays a stencil on the canvas: a ruler (straight edge to draw along) or an arc (circle or part of one) that pointer and agent strokes starting near it follow exactly, or a line (straight masking edge) or a shape (rect, ellipse or polygon template) that paint lands only inside of (outside with invert). Editor state saved with the document, not an undo step; replays keep the mask they were painted through. Returns the stencil with its id.',\n inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['ruler', 'arc', 'line', 'shape'], description: 'ruler, arc, line or shape.' }, ...STENCIL_GEOMETRY }, required: ['kind'], additionalProperties: false },\n annotations: { title: 'Lay a stencil', ...WRITE },\n handler: (a) => c.addStencil(a as Parameters<PaintEditorController[\"addStencil\"]>[0]),\n },\n {\n name: 'update_stencil',\n title: 'Change a stencil',\n description: 'Moves, reshapes, switches (guide or mask, invert) or hides (enabled false) a stencil by id from get_guides. Returns the stencil.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Stencil id from get_guides.' }, ...STENCIL_GEOMETRY }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Change a stencil', ...SET },\n handler: ({ id, ...update }) => c.updateStencil(id, update),\n },\n {\n name: 'remove_stencils',\n title: 'Take stencils away',\n description: 'Removes stencils by id, or every stencil when ids is missing. Returns the ids removed.',\n inputSchema: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string', description: 'Stencil id.' }, description: 'Stencil ids from get_guides (default all).' } }, additionalProperties: false },\n annotations: { title: 'Take stencils away', ...DESTRUCTIVE },\n handler: ({ ids }) => ({ removed: c.removeStencils(ids) }),\n },\n ];\n}\n","/**\n * Agent tools for wet media (watercolour and gouache on a simulated wet layer). Wet strokes are\n * painted with `paint_stroke` and a wet brush (`list_brushes` category `watercolor` or\n * `wet-tool`); these tools read and steer the wet paint. Every result says whether paint is still\n * wet. Settings (wet parameters, flow maps, tilt) are saved with the document but are not undo\n * steps; drying and wetting the paper are.\n */\n\nimport { WET_FLOW_KINDS } from '../paint/wet/flow';\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\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 SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst EMPTY = { type: 'object', properties: {}, additionalProperties: false } as const;\nconst LAYER_ID = { type: 'string', description: 'Pixel layer id from list_layers (default the active layer).' } as const;\nconst RECT = {\n type: 'object',\n description: 'A rectangle in document pixels.',\n properties: {\n x: { type: 'number', description: 'Left edge, document pixels.' },\n y: { type: 'number', description: 'Top edge, document pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in pixels.' },\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false,\n} as const;\n\nconst UNIT = (d: string) => ({ type: 'number', minimum: 0, maximum: 1, description: d }) as const;\n\nexport const WET_PARAM_SCHEMA = {\n viscosity: UNIT('How thick the water is: 0 runny like ink .. 1 very thick (default 0.3, watercolour).'),\n evaporation: UNIT('How fast it dries: 0 never .. 1 very fast (default 0.45).'),\n edgeDarkening: UNIT('Darker rims where a wash dries (default 0.5).'),\n pigmentWeight: UNIT('Heavy pigment travels less and settles sooner (default 0.3).'),\n granulation: UNIT('Pigment collecting in the paper’s valleys, a grainy look (default 0.4; needs a rough paper).'),\n pickup: UNIT('How readily water lifts settled pigment again (default 0.2).'),\n fixture: UNIT('How fast pigment settles into the paper while wet, i.e. staining (default 0.2).'),\n bloom: UNIT('Blooms: water running into a drying wash pushes its pigment into cauliflower edges (default 0.4).'),\n bleed: UNIT('Colour bleed between touching wet areas (default 0.3).'),\n} as const;\n\nexport function createPaintWetTools(c: PaintEditorController): AgentTool[] {\n const stillWet = () => c.wetState().wet;\n return [\n {\n name: 'get_wet_paint',\n title: 'Get the wet paint',\n description:\n 'Reads the wet media: whether any paint is still wet, the simulation clock (steps), whether time is paused, its speed (steps per second), the canvas tilt, the kernel running it (typescript, wasm, wasm-threads with the thread count, and a message when it runs slower than it could, e.g. without cross-origin isolation), and each wet layer: wet cells, where the water is (wetBounds), live tiles, its settings and flow map. Also engine: Engine B, the WebGPU accelerator (mode auto/off, state, reason, adapter, minTiles, activeTiles). Call it after wet strokes to see what is still wet.',\n inputSchema: EMPTY,\n annotations: { title: 'Get the wet paint', ...READ },\n handler: () => c.wetState(),\n },\n {\n name: 'advance_wet_paint',\n title: 'Let the wet paint flow',\n description:\n 'Lets time pass for wet paint: the water spreads, runs downhill on a tilted canvas, granulates, blooms and evaporates, and pigment settles. Give steps (simulation steps; at the default 120 per second, 120 steps is one second) or seconds (default 1). Not an undo step, as time passing never is. Returns the steps run, the clock, stillWet and each wet layer’s state. Paint dries by itself after a few thousand steps; dry_paint dries it at once.',\n inputSchema: {\n type: 'object',\n properties: {\n steps: { type: 'integer', minimum: 0, maximum: 100000, description: 'Simulation steps to run (default: seconds × speed).' },\n seconds: { type: 'number', minimum: 0, maximum: 600, description: 'Seconds of simulated time at the current speed (default 1; ignored when steps is given).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Let the wet paint flow', ...WRITE },\n handler: async ({ steps, seconds }) => {\n await c.wetIdle();\n return c.advanceWet({ steps, seconds });\n },\n },\n {\n name: 'dry_paint',\n title: 'Dry the wet paint',\n description:\n 'Dries wet paint at once: everything settles where it is and the water goes, as a hair dryer at full blast would (the same deterministic bake that saving uses). On one layer or every wet layer, optionally only inside rect. Dried washes stay put: later washes glaze over them instead of mixing in. One undo step. Returns dried (each layer id with the rectangle that dried) and stillWet.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Only this layer (default every wet layer).' }, rect: RECT }, additionalProperties: false },\n annotations: { title: 'Dry the wet paint', ...WRITE },\n handler: async ({ layerId, rect }) => {\n await c.wetIdle();\n return c.dryPaint({ layerId, rect });\n },\n },\n {\n name: 'wet_paper',\n title: 'Pre-wet the paper',\n description:\n 'Wets the paper with clear water over rect (default the whole canvas) on a layer, for wet-in-wet: paint laid on damp paper spreads softly with feathered edges instead of stopping at a hard edge. One undo step. Returns the layer id, the rectangle and wet: true. The paper dries again over time (advance_wet_paint) or at once (dry_paint).',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, rect: RECT, amount: { type: 'number', exclusiveMinimum: 0, maximum: 1, description: 'How wet, 0..1 (default 1: soaked).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Pre-wet the paper', ...WRITE },\n handler: async ({ layerId, rect, amount }) => {\n await c.wetIdle();\n return c.wetPaper({ layerId, rect, amount });\n },\n },\n {\n name: 'set_wet_settings',\n title: 'Set a layer’s wet settings',\n description:\n 'Changes how wet paint behaves on a layer: viscosity, evaporation, edge darkening, pigment weight, granulation, pickup (re-wetting), fixture (staining), blooms and colour bleed, each 0..1. Missing fields keep their values. Applies to the paint already wet too. Saved with the document, not an undo step. Returns the settings and stillWet.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, ...WET_PARAM_SCHEMA }, additionalProperties: false },\n annotations: { title: 'Set a layer’s wet settings', ...SET },\n handler: async ({ layerId, ...params }) => {\n await c.wetIdle();\n return { layerId: layerId ?? c.document.activeLayerId, params: c.setWetParams(layerId, params), stillWet: stillWet() };\n },\n },\n {\n name: 'set_flow_map',\n title: 'Set a flow map',\n description:\n 'Pushes a layer’s water in a pattern while it is wet: linear (one direction, angle), swirl (around center, clockwise or not), radial (out from center) or turbulent (eddies of scale pixels, seeded), at strength 0..1; none turns it off. Saved with the document, not an undo step. Returns the flow map and stillWet.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: [...WET_FLOW_KINDS], description: 'none, linear, swirl, radial or turbulent.' },\n strength: UNIT('How hard it pushes, 0..1 (1 is about a 25° tilt; default 0.5).'),\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'linear: direction in degrees clockwise from canvas down (default 0).' },\n center: {\n type: 'object',\n description: 'swirl / radial: centre in document pixels (default the canvas centre).',\n properties: { x: { type: 'number', description: 'Document x.' }, y: { type: 'number', description: 'Document y.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n clockwise: { type: 'boolean', description: 'swirl: turn clockwise (default true).' },\n scale: { type: 'number', minimum: 8, maximum: 1024, description: 'turbulent: eddy size in pixels (default 120).' },\n seed: { type: 'integer', minimum: 0, description: 'turbulent: pattern seed (default 1).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a flow map', ...SET },\n handler: async ({ layerId, ...flow }) => {\n await c.wetIdle();\n return { layerId: layerId ?? c.document.activeLayerId, flow: c.setFlowMap(layerId, flow), stillWet: stillWet() };\n },\n },\n {\n name: 'set_canvas_tilt',\n title: 'Tilt the canvas',\n description:\n 'Tilts the canvas so wet paint runs downhill: angle 0 (flat) .. 90 (upright) degrees, direction the way down in degrees clockwise from the canvas bottom (0 runs towards the bottom edge, 90 towards the left). Steep tilts with very wet paint make drips. Saved with the document, not an undo step. Returns the tilt and stillWet.',\n inputSchema: {\n type: 'object',\n properties: {\n angle: { type: 'number', minimum: 0, maximum: 90, description: 'Degrees from flat (default unchanged).' },\n direction: { type: 'number', minimum: -360, maximum: 360, description: 'Downhill direction, degrees clockwise from canvas down (default unchanged).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Tilt the canvas', ...SET },\n handler: async ({ angle, direction }) => {\n await c.wetIdle();\n return { tilt: c.setCanvasTilt({ angle, direction }), stillWet: stillWet() };\n },\n },\n {\n name: 'pause_wet_paint',\n title: 'Pause or resume wet paint',\n description: 'Stops time for wet paint (it stays wet and still) or lets it run again, and optionally sets the speed in steps per second. Editor state, not an undo step. Returns paused, speed and stillWet.',\n inputSchema: {\n type: 'object',\n properties: {\n paused: { type: 'boolean', description: 'true stops time, false resumes it.' },\n speed: { type: 'number', minimum: 1, maximum: 2000, description: 'Steps per second while running (default unchanged; 120 at start).' },\n },\n required: ['paused'],\n additionalProperties: false,\n },\n annotations: { title: 'Pause or resume wet paint', ...SET },\n handler: ({ paused, speed }) => {\n c.setWetPaused(paused);\n if (speed !== undefined) c.setWetSpeed(speed);\n const s = c.getState().wet;\n return { paused: s.paused, speed: s.speed, stillWet: s.wet };\n },\n },\n {\n name: 'set_wet_engine',\n title: 'Choose the wet-media engine',\n description:\n 'Lets Engine B, the WebGPU accelerator, run for large wet areas (gpu \"auto\", the default) or forces the CPU (gpu \"off\"), and optionally sets minTiles, the wet area at which the GPU takes over. The GPU only shades what is shown: the CPU engine stays the authority, so the painting, drying, saving, export and replay come out the same either way; force the CPU when the display looks wrong or the GPU is busy with other work. Editor setting, not an undo step, not saved with the painting. Returns the engine: mode, state (off, not-started, starting, unavailable, standby, active, failed), reason, adapter, maxTiles, minTiles and activeTiles.',\n inputSchema: {\n type: 'object',\n properties: {\n gpu: { type: 'string', enum: ['auto', 'off'], description: '\"auto\": use the GPU when there is one and the wet area is large; \"off\": the CPU only.' },\n minTiles: { type: 'integer', minimum: 1, maximum: 1024, description: 'Live 256 px tiles (all wet layers) at which the GPU takes over (default unchanged; 16 at start, a fully wet 1024² canvas).' },\n },\n required: ['gpu'],\n additionalProperties: false,\n },\n annotations: { title: 'Choose the wet-media engine', ...SET },\n handler: async ({ gpu, minTiles }) => ({ engine: await c.setWetEngine({ gpu, minTiles }) }),\n },\n {\n name: 'set_water_overlay',\n title: 'Show the water',\n description: 'Shows or hides the water overlay on the canvas (blue where the surface is wet, fainter where the paper is damp). View only, never in exports. Returns visible and stillWet.',\n inputSchema: { type: 'object', properties: { visible: { type: 'boolean', description: 'Show (true) or hide (false) the overlay.' } }, required: ['visible'], additionalProperties: false },\n annotations: { title: 'Show the water', ...SET },\n handler: ({ visible }) => ({ visible: c.setWaterOverlay(visible), stillWet: stillWet() }),\n },\n ];\n}\n","/**\n * Agent tools for thick paint and light: impasto strokes are painted with `paint_stroke` and an\n * impasto brush; these tools move the light, read and flatten a layer's relief, and switch instant\n * drying of wet strokes.\n */\n\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst LAYER_ID = { type: 'string', description: 'Pixel layer id from list_layers (default the active layer).' } as const;\n\nexport function createPaintThickTools(c: PaintEditorController): AgentTool[] {\n return [\n {\n name: 'set_lighting',\n title: 'Move the light',\n description:\n 'Sets where the light comes from for thick paint (impasto relief) and metallic paper: azimuth (degrees clockwise from the top of the canvas; 315 is top left), elevation (5 grazing, deep shadows .. 90 straight on, no relief) and relief (0 flat .. 1 strong). Nothing is repainted: the relief is lit at render time. Saved with the document, not an undo step. Returns the lighting.',\n inputSchema: {\n type: 'object',\n properties: {\n azimuth: { type: 'number', minimum: -360, maximum: 360, description: 'Light direction in degrees clockwise from the top of the canvas (default 315).' },\n elevation: { type: 'number', minimum: 5, maximum: 90, description: 'Light height above the canvas in degrees (default 40).' },\n relief: { type: 'number', minimum: 0, maximum: 1, description: 'How strongly the relief shows (default 0.7).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Move the light', ...SET },\n handler: (a) => c.setLighting(a),\n },\n {\n name: 'get_relief',\n title: 'Read a layer’s relief',\n description: 'Reads a layer’s thick paint: where the relief is (bounds, layer pixels), its highest point (0..255) and the lighting. relief is null when the layer is flat. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'Read a layer’s relief', ...READ },\n handler: ({ layerId }) => ({ relief: c.reliefOf(layerId), lighting: c.getLighting() }),\n },\n {\n name: 'flatten_relief',\n title: 'Flatten thick paint',\n description: 'Removes a layer’s relief and material (gloss, metallic): the paint keeps its colour but lies flat. One undo step. Returns the layer id and whether anything was flattened.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'Flatten thick paint', ...WRITE },\n handler: ({ layerId }) => c.flattenRelief(layerId),\n },\n {\n name: 'set_instant_dry',\n title: 'Dry wet strokes at once',\n description: 'Switches instant drying: on, every wet stroke (watercolour, gouache, oil) dries as soon as it ends, in the same undo step, so strokes stay exactly where they are laid; off (default), wet paint keeps moving until it dries. Editor state. Returns instantDry.',\n inputSchema: { type: 'object', properties: { on: { type: 'boolean', description: 'true dries every wet stroke as it ends.' } }, required: ['on'], additionalProperties: false },\n annotations: { title: 'Dry wet strokes at once', ...SET },\n handler: ({ on }) => ({ instantDry: c.setInstantDry(on) }),\n },\n ];\n}\n","/**\n * Agent tools for replaying a painting from its data: a time-lapse (frames, a video project of the\n * frames and the ffmpeg command that renders it) and a high-resolution export repainted from the\n * recorded strokes.\n */\n\nimport { bytesToBase64, decodePng, encodePng } from '../raster/png';\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\nimport { contactSheet } from './replay';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nexport function createPaintReplayTools(c: PaintEditorController): AgentTool[] {\n return [\n {\n name: 'export_time_lapse',\n title: 'Export a time-lapse',\n description:\n 'Repaints the painting from its recorded strokes and events (fills, clears, wet-media events at their clock) in a fresh editor and returns a time-lapse: frame count, fps, size, duration, a video project of the frames as stills (assets frame-0001.png…), and the ffmpeg command that renders it to timelapse.mp4 from the frame files (the video editor export path). Shows a contact sheet of a few frames. With includeFrames the frames come back as base64 PNGs too. Changes nothing. Layers with pixels that were not painted with recorded strokes (imported images, merges) start empty in the replay.',\n inputSchema: {\n type: 'object',\n properties: {\n frames: { type: 'integer', minimum: 2, maximum: 600, description: 'About how many frames (default 120); one after every few strokes.' },\n fps: { type: 'number', minimum: 1, maximum: 60, description: 'Frames per second of the video (default 12).' },\n maxSide: { type: 'integer', minimum: 32, maximum: 4096, description: 'Longest side of each frame in pixels (default 720).' },\n wetStepsPerFrame: { type: 'integer', minimum: 0, maximum: 10000, description: 'While wet paint flows between strokes, a frame every this many simulation steps (default 60; 0 none).' },\n holdMs: { type: 'number', minimum: 0, maximum: 10000, description: 'Hold the finished painting this long at the end, ms (default 1500).' },\n includeFrames: { type: 'boolean', description: 'Also return every frame as base64 PNG (large; default false).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Export a time-lapse', ...READ },\n handler: async (a) => {\n await c.wetIdle();\n const tl = await c.timeLapse({ frames: a.frames, fps: a.fps, maxSide: a.maxSide, wetStepsPerFrame: a.wetStepsPerFrame, holdMs: a.holdMs });\n const pick = [0, Math.floor(tl.frames.length / 3), Math.floor((2 * tl.frames.length) / 3), tl.frames.length - 1].filter((v, i, arr) => arr.indexOf(v) === i);\n const sheet = contactSheet(pick.map((i) => decodePng(tl.frames[i])), pick.length, 160);\n const info = {\n frames: tl.frames.length,\n fps: tl.fps,\n width: tl.width,\n height: tl.height,\n durationMs: tl.durationMs,\n ffmpeg: tl.ffmpeg.command,\n video: tl.video,\n ...(a.includeFrames ? { framesPng: tl.frames.map((f) => bytesToBase64(f)) } : {}),\n };\n return { content: [{ type: 'image', data: bytesToBase64(encodePng(sheet.width, sheet.height, sheet.data)), mimeType: 'image/png' }, { type: 'text', text: JSON.stringify(info) }] };\n },\n },\n {\n name: 'export_high_res',\n title: 'Export at a higher resolution',\n description:\n 'Repaints the painting from its recorded strokes at scale (2..8) times the size, with brush sizes, paper grain, fills and wet events scaled, so the export has real detail instead of upscaled pixels; wet paint is dried. Returns a PNG as base64 with its size. Changes nothing. Layers with pixels that were not painted with recorded strokes come out empty; the canvas may be at most 16384 px per side.',\n inputSchema: {\n type: 'object',\n properties: {\n scale: { type: 'number', minimum: 1, maximum: 8, description: 'How many times larger (e.g. 2 doubles width and height).' },\n paper: { type: 'boolean', description: 'Include the paper (default true).' },\n },\n required: ['scale'],\n additionalProperties: false,\n },\n annotations: { title: 'Export at a higher resolution', ...READ },\n handler: async ({ scale, paper }) => {\n await c.wetIdle();\n const img = await c.renderHighRes({ scale, paper });\n const bytes = encodePng(img.width, img.height, img.data);\n return { mimeType: 'image/png', width: img.width, height: img.height, bytes: bytes.length, data: bytesToBase64(bytes) };\n },\n },\n ];\n}\n","/**\n * Agent tools for the paint editor (`createPaintEditorToolProvider`). Strict schemas, every\n * argument described, all four annotations. Each write is one undo step (or editor state, said so)\n * and returns what it changed; ids come from the read tools (`list_layers`, `list_brushes`,\n * `list_papers`, `list_strokes`).\n */\n\nimport type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { MIXING_MODELS, type MixerLoad } from '../paint/mixer';\nimport { PAINT_GRADIENT_JSON_SCHEMA, PAINT_SOURCE_JSON_SCHEMA } from '../paint/gradient-paint';\nimport { BLEND_MODES, standardBlendMode, type BlendMode } from '../raster/blend';\nimport { bytesToBase64, encodePng } from '../raster/png';\nimport { PARTICLE_KINDS, PARTICLE_RENDERS } from './procedural';\nimport { STAMP_KINDS } from './stickers';\nimport { createPaintStickerTools } from './sticker-tools';\nimport { BRUSH_ROTATIONS, IMPASTO_TOOLS, MAX_BRUSH_SIZE, MULTI_COLOR_MODES, PAINT_BRUSH_CATEGORIES, PAINT_BRUSH_ENGINES, WET_MEDIA, WET_TOOLS, brushToJSON, isWetCapable, type PaintBrush } from './brushes';\nimport { MAX_CANVAS_SIDE } from './document';\nimport type { PaintEditorController } from './controller';\nimport type { PenPoint } from './engine';\nimport { GRAIN_PATTERNS, PAPER_FAMILIES, PAPER_LIBRARY } from './paper';\nimport { MAX_ZOOM, MIN_ZOOM } from './view';\nimport { createPaintGuideTools } from './guide-tools';\nimport { createPaintWetTools } from './wet-tools';\nimport { createPaintThickTools } from './thick-tools';\nimport { createPaintReplayTools } from './replay-tools';\nimport type { PaintAgentCommand, PaintDirector } from './presence';\nimport type { PresenceActorInfo } from '../animation/presence';\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 SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst EMPTY = { type: 'object', properties: {}, additionalProperties: false } as const;\nconst COLOR = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'Colour #rrggbb, e.g. \"#2a4b8d\".' } as const;\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers (default the active layer).' } as const;\n\nexport interface PaintEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `paint-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n /**\n * Acts out strokes, fills and replays with an agent cursor on the canvas (see `PaintDirector`):\n * strokes paint live along their points. Each call is still one undo step. Without it, they apply at once.\n */\n director?: PaintDirector;\n /** The agent's cursor (default `{ id: 'agent', name: 'Agent' }`). */\n actor?: PresenceActorInfo;\n}\n\nconst TIP = {\n type: 'object',\n description: 'The tip (the shared brush engine). Missing fields keep their current values.',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in document pixels.' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness, 0 (soft) .. 1 (hard).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (0.05 is smooth, 1 is dotted).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees, counter-clockwise; with rotation stroke or barrel it is added to the stroke direction or pen rotation.' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness: 1 round, lower is a flat ellipse (a pastel or chisel tip).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity ceiling: overlapping dabs never exceed it.' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (low flow builds up softly, like an airbrush).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Position smoothing of the input, 0..0.95.' },\n pressureSize: { type: 'boolean', description: 'Pressure scales the size (down to dynamics.minSize).' },\n pressureOpacity: { type: 'boolean', description: 'Pressure scales the opacity.' },\n blend: {\n type: 'string',\n enum: BLEND_MODES.map((m) => m.id),\n description: 'How paint combines with the layer. pigment mixes it in like real paint (blue into yellow gives green); multiply darkens like marker ink; watercolor glazes like a transparent wash; tint, highlight and shadow recolour, light and shade (see list_blend_modes).',\n },\n },\n additionalProperties: false,\n} as const;\n\nconst DYNAMICS = {\n type: 'object',\n description: 'Pen dynamics and jitter. Missing fields keep their current values.',\n properties: {\n minSize: { type: 'number', minimum: 0, maximum: 1, description: 'Size at zero pressure as a fraction of the full size (when pressureSize).' },\n pressureFlow: { type: 'number', minimum: 0, maximum: 1, description: 'How much pressure scales flow.' },\n tiltSize: { type: 'number', minimum: 0, maximum: 1, description: 'Laying the pen down widens the tip (up to 2.5×).' },\n tiltRoundness: { type: 'number', minimum: 0, maximum: 1, description: 'Laying the pen down flattens the tip along the tilt (side-of-the-pencil shading).' },\n tiltOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Laying the pen down lightens the mark.' },\n velocitySize: { type: 'number', minimum: -1, maximum: 1, description: 'Positive grows the tip on fast strokes, negative thins it (needs point times t).' },\n velocityOpacity: { type: 'number', minimum: -1, maximum: 1, description: 'Positive darkens fast marks, negative lightens them (needs point times t).' },\n sizeJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random size change per dab.' },\n angleJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random angle per dab, as a fraction of ±180°.' },\n opacityJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random opacity drop per dab.' },\n scatter: { type: 'number', minimum: 0, maximum: 4, description: 'Random offset across the stroke, in diameters.' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXER = {\n type: 'object',\n description: 'Mixer engine settings (the Mixer Brush). Missing fields keep their current values.',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: 'Pick-up ratio: how much canvas colour each dab picks up, 0 (dry) .. 1.' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'How much paint the reservoir holds; below 1 it drains per dab, so long strokes run dry.' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"Mix ratio: how far each dab's paint moves from the reservoir towards the picked-up colour.\" },\n loadEachStroke: { type: 'boolean', description: 'Refill the reservoir with the colour before each stroke.' },\n cleanEachStroke: { type: 'boolean', description: 'Empty the pickup well after each stroke.' },\n mixing: { type: 'string', enum: [...MIXING_MODELS], description: 'rgb: RGB averages. pigment: mixes like paint (Kubelka-Munk).' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXING = { type: 'string', enum: [...MIXING_MODELS], description: 'pigment: mixes like paint (Kubelka-Munk, blue and yellow make green); rgb: straight RGB averages.' } as const;\n\nconst BEHAVIOR = {\n type: 'object',\n description:\n 'How the brush handles paint on the canvas (dab and mixer engines; on a mixer brush pickup, mix, dirty and mixing are its mixer wet, mix, !cleanEachStroke and mixing). Missing fields keep their current values.',\n properties: {\n pickup: { type: 'number', minimum: 0, maximum: 1, description: 'Pick-up ratio 0..1: how much of the paint under each dab the brush lifts and carries along (0: none, a dry brush).' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"Mix ratio 0..1: how much of the carried paint goes into what the brush lays down, against the brush's own colour (scaled by pickup).\" },\n dirty: { type: 'boolean', description: 'Dirty brush: keep the carried paint after the stroke, so the next stroke starts with the last colour it touched (needs pickup above 0). clean_mixer_brush wipes it.' },\n mixing: MIXING,\n multicolor: {\n type: 'object',\n description: 'Multi-coloured brush (the older form of paint, the brush\\'s own gradient: tip is across-tip, dab is distance in dabs or random). Read back from paint when it has a multi-colour equivalent. Missing fields keep their current values.',\n properties: {\n mode: { type: 'string', enum: [...MULTI_COLOR_MODES], description: 'off; tip: the colours lie side by side across the tip (stripes along the stroke, like a brush loaded with several paints); dab: the colour changes along the stroke.' },\n colors: { type: 'array', maxItems: 8, items: { ...COLOR }, description: '2..8 colours #rrggbb; fewer than 2 uses the paint colour and the secondary colour.' },\n order: { type: 'string', enum: ['sequence', 'random'], description: 'dab mode: sequence runs through the colours in order, blending between them; random picks one per run of dabs (seeded).' },\n length: { type: 'integer', minimum: 1, maximum: 500, description: 'dab mode: dabs per colour (default 12).' },\n softness: { type: 'number', minimum: 0, maximum: 1, description: 'How softly neighbouring colours blend: 0 hard stripes or steps, 1 a smooth gradient.' },\n },\n additionalProperties: false,\n },\n },\n additionalProperties: false,\n} as const;\n\nconst BRUSH_PAINT = {\n description: \"The brush's own gradient paint, used instead of the editor's paint (null: the editor's paint). It is what the multi-colour settings became; stops may be \\\"paint\\\" / \\\"secondary\\\" for the editor's two colours.\",\n anyOf: [PAINT_GRADIENT_JSON_SCHEMA, { type: 'null', description: \"No gradient of its own: the editor's paint.\" }],\n} as const;\n\nconst BLENDER = {\n type: 'object',\n description: 'Blender engine settings. Missing fields keep their current values.',\n properties: {\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'How far each pixel under the tip moves towards the mix of the paint near it, per dab.' },\n distance: { type: 'number', minimum: 0, maximum: 1, description: 'Blend distance: how far away colours are mixed in, as a fraction of the tip diameter (0.35 default; higher blends wider and softer).' },\n mixing: MIXING,\n },\n additionalProperties: false,\n} as const;\n\nconst SMUDGE = {\n type: 'object',\n description: 'Smudge engine settings. Missing fields keep their current values.',\n properties: {\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'How strongly the dragged paint covers what is under the tip, per dab.' },\n length: { type: 'number', minimum: 0, maximum: 1, description: 'How far paint is dragged: the share of the carried paint kept from dab to dab (0.85 default; 1 drags it the whole stroke).' },\n mixing: { ...MIXING, description: 'rgb (default): smeared colours average; pigment: they mix like paint.' },\n },\n additionalProperties: false,\n} as const;\n\nconst WET_BRUSH = {\n type: 'object',\n description: 'Wet engine settings (watercolour, gouache and the wet tools). Missing fields keep their current values.',\n properties: {\n tool: {\n type: 'string',\n enum: [...WET_TOOLS],\n description:\n 'paint: water and pigment (picks up and mixes wet paint by behavior.pickup / mix); water: clear water (with rewet it lifts dried paint); dry: a hair dryer, takes water; blow: pushes the water along the stroke; blend: evens out wet pigment; smudge: drags wet paint; blot: a tissue, takes wet paint up (erase wet).',\n },\n medium: { type: 'string', enum: [...WET_MEDIA], description: \"The medium: watercolor, gouache or oil. A layer takes the medium's wet settings the first time it holds wet paint (oil: thick, barely flows, stays wet long and blends into the wet paint it meets).\" },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'Paint loading: 1 never runs out; lower, the brush runs dry along a long stroke (each stroke reloads).' },\n thinner: { type: 'number', minimum: 0, maximum: 1, description: 'Thinner (water or solvent): more transparent, runnier paint.' },\n water: { type: 'number', minimum: 0, maximum: 1, description: 'Wetness: water laid per stroke, 0.2 a damp brush .. 1 a dripping one.' },\n pigment: { type: 'number', minimum: 0, maximum: 1, description: 'Pigment concentration: 0 clear water .. 1 straight from the tube.' },\n body: { type: 'number', minimum: 0, maximum: 1, description: 'Body: 0 a transparent watercolour (glazes) .. 1 opaque gouache (hides what is under it).' },\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of dry, blow, blend, smudge and blot.' },\n rewet: { type: 'number', minimum: 0, maximum: 1, description: 'How much dried paint the water lifts again: 0 none (staining) .. 1.' },\n },\n additionalProperties: false,\n} as const;\n\nconst IMPASTO_BRUSH = {\n type: 'object',\n description: 'Impasto engine settings (thick paint with relief, lit by set_lighting). Missing fields keep their current values.',\n properties: {\n tool: {\n type: 'string',\n enum: [...IMPASTO_TOOLS],\n description: 'brush: thick paint with bristle ridges along the stroke; tube: blobs squeezed from the tube; knife: a palette knife that scrapes the relief flat and smears the paint; roller: a flat stippled band; gloop: a thick, glossy, rounded bead.',\n },\n thickness: { type: 'number', minimum: 0, maximum: 1, description: 'How far the paint stands up.' },\n gloss: { type: 'number', minimum: 0, maximum: 1, description: 'Shine: 0 matte .. 1 wet-looking gloss.' },\n metallic: { type: 'number', minimum: 0, maximum: 1, description: 'Metallic paint: highlights take the paint colour (gold, copper, silver).' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'Paint loading: 1 never runs out; lower, the stroke thins and fades as the brush empties.' },\n thinner: { type: 'number', minimum: 0, maximum: 1, description: 'Thinner: flatter, more transparent paint.' },\n drag: { type: 'number', minimum: 0, maximum: 1, description: 'Knife and brush: how much of the paint under the tool is dragged along.' },\n },\n additionalProperties: false,\n} as const;\n\nconst PARTICLE_BRUSH = {\n type: 'object',\n description: 'Particle engine settings (procedural brushes). Missing fields keep their current values.',\n properties: {\n kind: { type: 'string', enum: [...PARTICLE_KINDS], description: 'string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade.' },\n render: { type: 'string', enum: [...PARTICLE_RENDERS], description: 'dot: a mark at each step; line: each particle draws a continuous line; sequence: beads spaced along each path.' },\n count: { type: 'integer', minimum: 1, maximum: 64, description: 'Particles (flow: how many are thrown per tip of travel).' },\n size: { type: 'number', minimum: 0, maximum: 1, description: 'Mark size as a fraction of the tip size.' },\n spread: { type: 'number', minimum: 0, maximum: 1, description: 'How far apart the particles start, as a fraction of the tip size.' },\n stiffness: { type: 'number', minimum: 0, maximum: 1, description: 'Spring and string stiffness: stiff follows closely, soft swings wide.' },\n damping: { type: 'number', minimum: 0, maximum: 1, description: 'How quickly motion dies down.' },\n speed: { type: 'number', minimum: 0, maximum: 1, description: 'Orbit and flow speed.' },\n gravity: { type: 'number', minimum: -1, maximum: 1, description: 'Pull downwards (negative: upwards).' },\n life: { type: 'number', minimum: 0, maximum: 1, description: 'Flow particles: how long they live.' },\n interval: { type: 'number', minimum: 0, maximum: 1, description: 'Sequence: spacing of the beads.' },\n },\n additionalProperties: false,\n} as const;\n\nconst BRISTLE_BRUSH = {\n type: 'object',\n description: 'Bristle engine settings: a tuft of bristles, each its own line. Missing fields keep their current values.',\n properties: {\n count: { type: 'integer', minimum: 3, maximum: 128, description: 'Bristles in the tuft.' },\n stiffness: { type: 'number', minimum: 0, maximum: 1, description: 'Stiff bristles follow the tip; soft ones lag and bend into curves at turns.' },\n splay: { type: 'number', minimum: 0, maximum: 1, description: 'How far pressure spreads the tuft.' },\n thickness: { type: 'number', minimum: 0, maximum: 1, description: \"Each bristle's line width as a fraction of the tip size.\" },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'Paint per bristle: 1 never runs dry; lower, bristles run out along the stroke (dry brush).' },\n jitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random variation between bristles.' },\n },\n additionalProperties: false,\n} as const;\n\nconst STICKER_BRUSH = {\n type: 'object',\n description: 'Sticker engine settings: stamps sprayed along the stroke as editable stickers. Missing fields keep their current values.',\n properties: {\n stamps: { type: 'array', minItems: 1, maxItems: 9, items: { type: 'string', enum: [...STAMP_KINDS], description: 'A stamp.' }, description: 'The stamps to spray (one picked at random per sticker).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Stickers per distance travelled.' },\n sizeJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random size change.' },\n rotationJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random turn as a fraction of ±180°.' },\n scatter: { type: 'number', minimum: 0, maximum: 2, description: 'How far from the path stickers land, in tip diameters.' },\n colorJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random colour change around the paint colour.' },\n },\n additionalProperties: false,\n} as const;\n\nconst BRUSH_FIELDS = {\n name: { type: 'string', minLength: 1, maxLength: 80, description: 'Display name.' },\n category: { type: 'string', enum: [...PAINT_BRUSH_CATEGORIES], description: 'Library category.' },\n engine: {\n type: 'string',\n enum: [...PAINT_BRUSH_ENGINES],\n description: 'dab: paints dabs (with behavior.pickup or gradient paint it paints dab by dab, lifting and mixing paint); eraser: removes paint; mixer: the Mixer Brush (a reservoir plus picked-up paint); blender: mixes the colours under and near the tip; smudge: drags paint along the stroke; wet: watercolour, gouache, oil and the wet tools on a simulated wet layer (see wet); impasto: thick paint with relief (see impasto); particle: procedural particle brushes (see particle); bristle: a tuft of bristles that bend and splay (see bristle); sticker: sprays editable sticker stamps (see sticker).',\n },\n tip: TIP,\n grain: { type: 'number', minimum: 0, maximum: 1, description: 'Paper grain response: 0 ignores the grain, 1 catches only its peaks at light pressure (pencil, pastel, charcoal). Multiplied by the paper depth.' },\n pressureSmoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Pressure smoothing, separate from the tip position smoothing.' },\n rotation: { type: 'string', enum: [...BRUSH_ROTATIONS], description: 'What turns the tip: fixed (tip.angle), stroke (follows the direction of travel) or barrel (the pen barrel rotation, twist).' },\n dynamics: DYNAMICS,\n mixer: MIXER,\n loadsColor: { type: 'boolean', description: 'Mixer engine: load the colour each stroke (false for a mixer that only moves paint).' },\n behavior: BEHAVIOR,\n blender: BLENDER,\n smudge: SMUDGE,\n wet: WET_BRUSH,\n impasto: IMPASTO_BRUSH,\n particle: PARTICLE_BRUSH,\n bristle: BRISTLE_BRUSH,\n sticker: STICKER_BRUSH,\n paint: BRUSH_PAINT,\n} as const;\n\nconst POINTS = {\n type: 'array',\n minItems: 1,\n maxItems: 20000,\n description: 'The path in document pixels (raw samples; the brush smooths them). One point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in pixels.' },\n y: { type: 'number', description: 'Document y in pixels.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure 0..1 (default 1).' },\n t: { type: 'number', description: 'Time in milliseconds (optional; drives velocity dynamics, e.g. an ink pen thinning on fast strokes).' },\n tiltX: { type: 'number', minimum: -90, maximum: 90, description: 'Pen tilt in degrees towards +x (Pointer Events tiltX).' },\n tiltY: { type: 'number', minimum: -90, maximum: 90, description: 'Pen tilt in degrees towards +y (Pointer Events tiltY).' },\n altitude: { type: 'number', minimum: 0, maximum: 1.5708, description: 'Pen altitude in radians: 1.5708 upright, 0 lying flat (overrides tiltX / tiltY).' },\n azimuth: { type: 'number', minimum: 0, maximum: 6.2832, description: 'Where the pen leans, radians clockwise from +x (with altitude).' },\n twist: { type: 'number', minimum: 0, maximum: 359, description: 'Barrel rotation in degrees (brushes with rotation barrel).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n} as const;\n\nfunction loadResult(load: MixerLoad) {\n const hex = (c: readonly number[] | null) => (c ? '#' + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('') : null);\n return { reservoir: hex(load.reservoir), amount: Math.round(load.amount * 1000) / 1000, pickup: hex(load.pickup) };\n}\n\nfunction brushSummary(b: PaintBrush) {\n return {\n gradient: b.paint !== null,\n id: b.id,\n name: b.name,\n category: b.category,\n engine: b.engine,\n size: b.tip.size,\n grain: b.grain,\n rotation: b.rotation,\n wetCapable: isWetCapable(b),\n pickup: b.behavior.pickup,\n dirty: b.behavior.dirty,\n multicolor: b.behavior.multicolor.mode,\n ...(b.engine === 'wet' ? { wetTool: b.wet.tool, medium: b.wet.medium } : {}),\n ...(b.engine === 'impasto' ? { impastoTool: b.impasto.tool } : {}),\n builtin: Boolean(b.builtin),\n };\n}\n\n/** Agent tools for one paint editor. */\nexport function createPaintEditorToolProvider(controller: PaintEditorController, options: PaintEditorToolProviderOptions = {}): AgentToolProvider {\n const c = controller;\n const tools: AgentTool[] = [];\n const add = (t: AgentTool) => tools.push(t);\n /** Runs `command` through the director when there is one (its error becomes the tool error), else `direct`. */\n const act = async <T>(command: PaintAgentCommand, direct: () => T): Promise<T> => {\n if (!options.director) return direct();\n const [r] = await options.director.perform(options.actor ?? { id: 'agent', name: 'Agent' }, [command]);\n if (!r || !r.ok) throw new Error(r && !r.ok ? r.message : 'The director did not run the command.');\n return r.result as T;\n };\n\n // --- Canvas ------------------------------------------------------------------\n\n add({\n name: 'get_canvas_info',\n title: 'Get the canvas',\n description:\n 'Reads the canvas: size in pixels and dpi, the paper (preset, colour, grain depth), the active layer, the active brush id, the paint colour, the view (zoom, rotation, mirror), the hand, how many strokes are recorded, the undo / redo labels, and the blend modes. Call it first.',\n inputSchema: EMPTY,\n annotations: { title: 'Get the canvas', ...READ },\n handler: () => {\n const s = c.getState();\n const d = s.document;\n return {\n width: d.canvas.width,\n height: d.canvas.height,\n dpi: d.canvas.dpi,\n paper: { preset: d.paper.preset, name: d.paper.name, color: d.paper.color, depth: d.paper.depth },\n activeLayerId: d.activeLayerId,\n layerCount: c.listLayers().length,\n brushId: s.brush.id,\n color: s.color,\n secondaryColor: s.secondaryColor,\n view: { zoom: s.view.zoom, rotation: s.view.rotation, mirror: s.view.mirror },\n hand: s.hand,\n strokes: d.strokes.length,\n undo: s.undoLabel,\n redo: s.redoLabel,\n blendModes: BLEND_MODES.map((m) => m.id),\n wet: s.wet.wet,\n lighting: c.getLighting(),\n };\n },\n });\n\n add({\n name: 'new_canvas',\n title: 'Start a new canvas',\n description:\n 'Replaces the document with a blank canvas: one empty layer over the paper. Size and dpi default to the current ones, the paper to the current paper (or a preset id from list_papers). One undo step (undo brings the old painting back). Returns the new size, paper and layer id.',\n inputSchema: {\n type: 'object',\n properties: {\n width: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Width in pixels (default the current width).' },\n height: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Height in pixels (default the current height).' },\n dpi: { type: 'number', minimum: 1, maximum: 10000, description: 'Pixels per inch for print sizes and PSD (default the current dpi).' },\n paper: { type: 'string', description: 'Paper preset id from list_papers (default the current paper).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Start a new canvas', ...DESTRUCTIVE },\n handler: (a) => {\n const d = c.newCanvas({ width: a.width, height: a.height, dpi: a.dpi, paper: a.paper });\n return { width: d.canvas.width, height: d.canvas.height, dpi: d.canvas.dpi, paper: d.paper.preset, layerId: d.activeLayerId };\n },\n });\n\n add({\n name: 'set_canvas',\n title: 'Resize the canvas',\n description:\n 'Changes the canvas size (pixels stay where they are, anchored at the top-left; anything outside is kept on the layers but not shown) and / or its dpi. One undo step. Returns the canvas.',\n inputSchema: {\n type: 'object',\n properties: {\n width: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Width in pixels (default unchanged).' },\n height: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Height in pixels (default unchanged).' },\n dpi: { type: 'number', minimum: 1, maximum: 10000, description: 'Pixels per inch (default unchanged).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Resize the canvas', ...SET },\n handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi }),\n });\n\n // --- Paper -------------------------------------------------------------------\n\n add({\n name: 'list_papers',\n title: 'List papers',\n description:\n 'Lists the built-in paper library (generated grain, no textures): id, name, family, grain pattern, colour, grain depth and the wet-media properties (absorption, permeability, roughness). Use an id with set_paper or new_canvas.',\n inputSchema: EMPTY,\n annotations: { title: 'List papers', ...READ },\n handler: () => ({\n papers: PAPER_LIBRARY.map((p) => ({ id: p.preset, name: p.name, family: p.family, pattern: p.pattern, color: p.color, depth: p.depth, absorption: p.absorption, permeability: p.permeability, roughness: p.roughness })),\n families: [...PAPER_FAMILIES],\n patterns: [...GRAIN_PATTERNS],\n }),\n });\n\n add({\n name: 'get_paper',\n title: 'Get the paper',\n description: 'Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness).',\n inputSchema: EMPTY,\n annotations: { title: 'Get the paper', ...READ },\n handler: () => JSON.parse(JSON.stringify(c.document.paper)),\n });\n\n const tuning = {\n type: 'object',\n properties: {\n contrast: { type: 'number', minimum: 0, maximum: 3, description: 'Height contrast around the middle (default 1).' },\n invert: { type: 'boolean', description: 'Swap peaks and valleys.' },\n },\n additionalProperties: false,\n } as const;\n\n add({\n name: 'set_paper',\n title: 'Set the paper',\n description:\n 'Changes the paper under the whole painting: a preset from list_papers and / or its settings. Grain depth decides how much grain-responsive brushes (pencil, pastel, charcoal) skip the valleys; changing it re-renders the paper but not strokes already painted. One undo step. Returns the paper.',\n inputSchema: {\n type: 'object',\n properties: {\n preset: { type: 'string', description: 'Paper id from list_papers (its settings, then the other fields here on top).' },\n color: { ...COLOR, description: 'Paper colour #rrggbb.' },\n depth: { type: 'number', minimum: 0, maximum: 1, description: 'Grain depth 0..1 (0: brushes ignore the grain).' },\n scale: { type: 'number', minimum: 0.25, maximum: 8, description: 'Grain scale: 1 natural, 2 twice as coarse.' },\n seed: { type: 'integer', minimum: 0, description: 'Grain seed: another sheet of the same paper.' },\n pattern: { type: 'string', enum: [...GRAIN_PATTERNS], description: 'Grain pattern.' },\n tiling: { type: 'string', enum: ['repeat', 'mirror'], description: 'How the grain tile repeats (mirror hides seams).' },\n shading: { type: 'number', minimum: 0, maximum: 1, description: 'How much the grain shades the paper colour.' },\n dry: { ...tuning, description: 'Height map tuning for dry media.' },\n wet: { ...tuning, description: 'Height map tuning for wet media (the grain water and pigment follow).' },\n absorption: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how fast the paper soaks up water (high: washes stop spreading sooner).' },\n permeability: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how freely water flows over the fibres (low: more ragged, slower spreading).' },\n roughness: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how much the grain pins wet edges and catches granulating pigment.' },\n metallic: { type: 'number', minimum: 0, maximum: 1, description: 'Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light (see set_lighting). 0 by default.' },\n name: { type: 'string', minLength: 1, description: 'Display name.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the paper', ...SET },\n handler: (a) => c.setPaper(a),\n });\n\n // --- Layers ------------------------------------------------------------------\n\n add({\n name: 'list_layers',\n title: 'List layers',\n description:\n 'Lists the layers top first: id, name, type (pixel or group), visibility, opacity, blend, clipping, locks, whether it is active, the painted bounds in document pixels (null when empty), parent and depth.',\n inputSchema: EMPTY,\n annotations: { title: 'List layers', ...READ },\n handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId }),\n });\n\n add({\n name: 'add_layer',\n title: 'Add a layer',\n description: 'Adds an empty pixel layer (or a group) above the active layer, or above aboveId, or at the top of parentId; it becomes the active layer. One undo step. Returns the layer.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', minLength: 1, description: 'Layer name (default \"Layer n\").' },\n type: { type: 'string', enum: ['pixel', 'group'], description: 'pixel (default) or group.' },\n aboveId: { type: 'string', description: 'Put it directly above this layer (default the active layer).' },\n parentId: { type: 'string', description: 'Put it at the top of this group instead.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer', ...WRITE },\n handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId }),\n });\n\n add({\n name: 'update_layer',\n title: 'Change a layer',\n description: 'Changes a layer: name, visibility, opacity, blend mode, clipping to the layer below, locks. One undo step. Returns the layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Layer id from list_layers.' },\n name: { type: 'string', minLength: 1, description: 'New name.' },\n visible: { type: 'boolean', description: 'Show or hide.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n blend: {\n type: 'string',\n enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'],\n description: 'Blend mode (pass-through for groups only). Painterly modes: watercolor (the layer glazes like a transparent wash), tint (recolours keeping lightness), highlight (adds soft coloured light), shadow (shades with ambient fill); see list_blend_modes.',\n },\n clipped: { type: 'boolean', description: 'Clip to the layer below (paint only shows where it has paint).' },\n locks: {\n type: 'object',\n description: 'Locks to change (others keep their values).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting.' },\n transparency: { type: 'boolean', description: 'Paint only where the layer already has paint.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'All of the above, plus property changes and removal.' },\n },\n additionalProperties: false,\n },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer', ...SET },\n handler: ({ layerId, ...u }) => c.updateLayer(layerId, u as { blend?: BlendMode }),\n });\n\n add({\n name: 'list_blend_modes',\n title: 'List blend modes',\n description:\n 'Lists every layer and brush blend mode: id, label, group (standard Photoshop modes, pigment, or painterly), what the pigment and painterly modes do, and the standard mode PSD export writes in their place. Changes nothing. Read it before choosing a painterly mode for update_layer or a brush tip.',\n inputSchema: EMPTY,\n annotations: { title: 'List blend modes', ...READ },\n handler: () => ({\n modes: BLEND_MODES.map((m) => ({\n id: m.id,\n label: m.label,\n group: m.painterly ? 'painterly' : m.id === 'pigment' ? 'pigment' : 'standard',\n ...(m.description ? { description: m.description } : {}),\n psd: standardBlendMode(m.id),\n })),\n }),\n });\n\n add({\n name: 'set_active_layer',\n title: 'Choose the active layer',\n description: 'Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Layer id from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Choose the active layer', ...SET },\n handler: ({ layerId }) => {\n c.setActiveLayer(layerId);\n return c.describeLayer(c.getLayer(layerId)!);\n },\n });\n\n add({\n name: 'remove_layer',\n title: 'Remove a layer',\n description: 'Removes a layer (a group with its children). One undo step. Returns the removed id and the new active layer.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Layer id from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Remove a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => c.removeLayer(layerId),\n });\n\n add({\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Layer id from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId }) => c.duplicateLayer(layerId),\n });\n\n add({\n name: 'move_layer',\n title: 'Reorder a layer',\n description: 'Moves a layer to a position in its group or another (0 is the bottom). One undo step. Returns the layer with its new parent.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Layer id from list_layers.' },\n index: { type: 'integer', minimum: 0, description: 'New position among its siblings, 0 = bottom.' },\n parentId: { type: ['string', 'null'], description: 'Group id to move into, or null for the top level (default its current parent).' },\n },\n required: ['layerId', 'index'],\n additionalProperties: false,\n },\n annotations: { title: 'Reorder a layer', ...WRITE },\n handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId),\n });\n\n add({\n name: 'merge_layer_down',\n title: 'Merge a layer down',\n description: 'Merges a pixel layer into the pixel layer directly below it, which keeps its name, opacity and blend. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'The upper layer, from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: ({ layerId }) => c.mergeDown(layerId),\n });\n\n add({\n name: 'fill_layer',\n title: 'Fill a layer',\n description:\n 'Fills a whole pixel layer with a colour: a flat block-in, or with grain above 0 a fill that follows the paper grain like pastel or charcoal rubbed across the sheet (pressure decides how far it reaches into the valleys). Good for a ground colour or a tonal layer to paint over. One undo step. Returns the layer id and the filled bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n color: { ...COLOR, description: 'Fill colour #rrggbb (default the paint colour).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1 (default 1).' },\n grain: { type: 'number', minimum: 0, maximum: 1, description: 'Paper grain response 0..1 (default 0: flat).' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'With grain: how far into the valleys, 0..1 (default 0.5).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal; pigment mixes like paint, watercolor glazes; see list_blend_modes).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill a layer', ...WRITE },\n handler: (a) => act({ kind: 'fill', fill: a }, () => c.fillLayer(a)),\n });\n\n add({\n name: 'clear_layer',\n title: 'Clear a layer',\n description: 'Removes every pixel of a pixel layer. One undo step. Returns the layer id and the bounds that were cleared (null if it was empty).',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'Clear a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => c.clearLayer(layerId),\n });\n\n // --- Brushes -----------------------------------------------------------------\n\n add({\n name: 'list_brushes',\n title: 'List brushes',\n description:\n 'Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.',\n inputSchema: EMPTY,\n annotations: { title: 'List brushes', ...READ },\n handler: () => {\n const s = c.getState();\n return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(c.getBrush(s.brush.id)?.builtin), brushes: s.brushes.map(brushSummary) };\n },\n });\n\n add({\n name: 'get_brush',\n title: 'Get a brush',\n description:\n \"Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer, behavior: pick-up / mix / dirty, blender, smudge, its own gradient paint as paint, null when it paints the editor's paint). Without id, the active brush as edited. Also returns strokePaint: what a stroke with it would paint now (its gradient, else the editor's paint from set_paint).\",\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Brush id from list_brushes (default the active brush).' } }, additionalProperties: false },\n annotations: { title: 'Get a brush', ...READ },\n handler: ({ id }) => {\n const st = c.getState();\n if (id === undefined) return { ...brushToJSON(st.brush), strokePaint: st.brush.paint ?? st.paint };\n const b = c.getBrush(id);\n if (!b) throw new Error(`Unknown brush \"${id}\". Call list_brushes for the ids.`);\n return { ...brushToJSON(b), builtin: Boolean(b.builtin), strokePaint: b.paint ?? st.paint };\n },\n });\n\n add({\n name: 'set_brush',\n title: 'Set the brush',\n description:\n 'Chooses the active brush (id) and / or changes its settings; strokes without a brush use it. Editor state, not an undo step. Changes stay on the active brush until save_brush stores them. Returns the active brush.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Brush id from list_brushes to start from.' }, ...BRUSH_FIELDS }, additionalProperties: false },\n annotations: { title: 'Set the brush', ...SET },\n handler: (a) => brushToJSON(c.setBrush(a)),\n });\n\n add({\n name: 'save_brush',\n title: 'Save a brush',\n description: 'Saves the active brush (with its changes) into the user library under a new or existing user id. Built-in ids are refused. Library state, not an undo step. Returns the saved brush.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$', description: 'Library id: lowercase letters, digits and dashes.' },\n name: { type: 'string', minLength: 1, maxLength: 80, description: 'Display name (default the brush name).' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n annotations: { title: 'Save a brush', ...SET },\n handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name })),\n });\n\n add({\n name: 'remove_brush',\n title: 'Remove a saved brush',\n description: 'Removes a user brush from the library (built-in brushes cannot be removed). Library state, not an undo step. Returns the removed id.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'User brush id from list_brushes.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Remove a saved brush', ...DESTRUCTIVE },\n handler: ({ id }) => {\n c.removeBrush(id);\n return { removed: id };\n },\n });\n\n add({\n name: 'export_brushes',\n title: 'Export brushes',\n description: 'Returns a brush library file (JSON: format pixerate-brushes, version 1, brushes) of the given brushes, or of every user brush. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: { ids: { type: 'array', items: { type: 'string', description: 'Brush id.' }, description: 'Brush ids from list_brushes (default every user brush).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Export brushes', ...READ },\n handler: ({ ids }) => c.exportBrushes(ids),\n });\n\n add({\n name: 'import_brushes',\n title: 'Import brushes',\n description:\n 'Adds the brushes of a brush library file (as export_brushes returns) to the user library. Taken ids are skipped unless replace is true; built-in ids are always skipped. Library state, not an undo step. Returns the ids added and skipped and any problems.',\n inputSchema: {\n type: 'object',\n properties: {\n library: { type: 'object', description: 'The library file: {format: \"pixerate-brushes\", version: 1, brushes: [...]}.' },\n replace: { type: 'boolean', description: 'Replace user brushes with the same id (default false).' },\n },\n required: ['library'],\n additionalProperties: false,\n },\n annotations: { title: 'Import brushes', ...WRITE },\n handler: ({ library, replace }) => c.importBrushes(library, { replace }),\n });\n\n add({\n name: 'render_brush_thumbnail',\n title: 'Render a brush thumbnail',\n description: 'Renders a brush as a PNG: a test stroke (an S-curve swelling and fading in pressure) on the current paper, so you can see what it does before painting. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Brush id from list_brushes (default the active brush).' },\n width: { type: 'integer', minimum: 16, maximum: 512, description: 'Width in pixels (default 160).' },\n height: { type: 'integer', minimum: 16, maximum: 512, description: 'Height in pixels (default 48).' },\n color: { ...COLOR, description: 'Stroke colour (default the paint colour).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Render a brush thumbnail', ...READ },\n handler: ({ id, width, height, color }) => {\n const t = c.brushThumbnail(id ?? c.getState().brush.id, { width, height, color });\n return {\n content: [\n { type: 'image', data: bytesToBase64(encodePng(t.width, t.height, t.data)), mimeType: 'image/png' },\n { type: 'text', text: `Brush ${id ?? c.getState().brush.id} ${t.width}×${t.height}` },\n ],\n };\n },\n });\n\n // --- Colour and the Mixer Brush ----------------------------------------------\n\n add({\n name: 'set_color',\n title: 'Set the paint colour',\n description: 'Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Editor state, not an undo step. Returns both colours.',\n inputSchema: {\n type: 'object',\n properties: { color: { ...COLOR, description: 'Paint colour #rrggbb.' }, secondaryColor: { ...COLOR, description: 'Second colour #rrggbb.' } },\n additionalProperties: false,\n },\n annotations: { title: 'Set the paint colour', ...SET },\n handler: ({ color, secondaryColor }) => {\n if (color !== undefined) c.setColor(color);\n if (secondaryColor !== undefined) c.setSecondaryColor(secondaryColor);\n const s = c.getState();\n return { color: s.color, secondaryColor: s.secondaryColor };\n },\n });\n\n add({\n name: 'set_paint',\n title: 'Set the paint (colour or gradient)',\n description:\n 'Sets what strokes paint with: one colour, or gradient paint that changes colour as the stroke goes. Gradient paint: 2..16 stops sorted by offset (#rrggbb, or \"paint\" / \"secondary\" for the two editor colours), space pigment (default: mixes like paint) / oklab / srgb, and map: distance (default: the colour changes as the stroke is dragged out, 300 px then mirrored back), pressure, tilt, velocity, direction, time, across-tip or random. Every stroking brush samples it per dab (dab, mixer, watercolour and gouache, oil and impasto, particle, bristle, sticker); erasers, blenders and smudges ignore it, and a brush with its own gradient (get_brush paint) paints that instead. set_color goes back to one colour. Editor state, not an undo step. Returns paint, color and secondaryColor.',\n inputSchema: { type: 'object', properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ['paint'], additionalProperties: false },\n annotations: { title: 'Set the paint (colour or gradient)', ...SET },\n handler: ({ paint }) => {\n const p = c.setPaint(paint);\n const s = c.getState();\n return { paint: p, color: s.color, secondaryColor: s.secondaryColor };\n },\n });\n\n add({\n name: 'pick_color',\n title: 'Pick a colour',\n description: 'Reads the colour at a document pixel: of the painting with its paper, or of one layer (with its alpha). Changes nothing. Returns #rrggbb and alpha 0..1.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in pixels.' },\n y: { type: 'number', description: 'Document y in pixels.' },\n layerId: { type: 'string', description: 'Pick from this layer only (default the painting as shown).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Pick a colour', ...READ },\n handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId }),\n });\n\n add({\n name: 'get_mixer_brush',\n title: 'Get the Mixer Brush load',\n description:\n \"Reads what the brush carries between strokes: reservoir #rrggbb or null (the Mixer Brush's loaded paint), amount 0..1 left, pickup #rrggbb or null (paint picked up from the canvas, kept by dirty brushes and a Mixer Brush that does not clean each stroke). Call before painting with mixer-brush when its loadEachStroke is false, or with a dirty brush.\",\n inputSchema: EMPTY,\n annotations: { title: 'Get the Mixer Brush load', ...READ },\n handler: () => loadResult(c.getState().mixerLoad),\n });\n\n add({\n name: 'load_mixer_brush',\n title: 'Load the Mixer Brush',\n description: 'Fills the Mixer Brush reservoir with a colour (amount 1), keeping what it picked up. Editor state, not an undo step. Returns the load.',\n inputSchema: { type: 'object', properties: { color: { ...COLOR, description: 'Colour #rrggbb (default the paint colour).' } }, additionalProperties: false },\n annotations: { title: 'Load the Mixer Brush', ...SET },\n handler: ({ color }) => loadResult(c.loadMixerBrush(color)),\n });\n\n add({\n name: 'clean_mixer_brush',\n title: 'Clean the Mixer Brush',\n description: 'Cleans the brush: empties the picked-up paint a dirty brush or the Mixer Brush carries (and with emptyReservoir the Mixer Brush paint too). Editor state, not an undo step. Returns the load.',\n inputSchema: { type: 'object', properties: { emptyReservoir: { type: 'boolean', description: 'Also drop the loaded paint (default false).' } }, additionalProperties: false },\n annotations: { title: 'Clean the Mixer Brush', ...SET },\n handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true)),\n });\n\n // --- Painting ----------------------------------------------------------------\n\n add({\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer’s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.',\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n layerId: LAYER_ID,\n brushId: { type: 'string', description: 'Brush id from list_brushes (default the active brush).' },\n brush: { type: 'object', description: 'Brush changes for this stroke only.', properties: BRUSH_FIELDS, additionalProperties: false },\n color: { ...COLOR, description: 'Paint colour #rrggbb (default the paint colour).' },\n paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: \"What this stroke paints with instead of the editor's paint: a colour, or gradient paint that changes colour as the stroke goes (by distance, pressure, tilt, speed, direction, time, across the tip or at random). Wins over the brush's own gradient.\" },\n seed: { type: 'integer', minimum: 0, description: 'Jitter seed (default a new one); the same seed and points give the same marks.' },\n snapToPerspective: { type: 'boolean', description: 'Snap the points to the perspective guide, as a pen stroke would (default false).' },\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: async (a) => {\n await c.wetIdle();\n const stroke = { points: a.points as PenPoint[], layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };\n const r = await act({ kind: 'stroke', stroke }, () => c.paintStroke(stroke));\n return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;\n },\n });\n\n add({\n name: 'list_strokes',\n title: 'List recorded strokes',\n description:\n 'Lists the recorded strokes, oldest first (every stroke is kept as data): id, layer, brush id and name, colour, point count, bounds and seed. With ids, also the points. Use the ids with replay_strokes.',\n inputSchema: {\n type: 'object',\n properties: {\n ids: { type: 'array', items: { type: 'string', description: 'Stroke id.' }, description: 'Only these strokes, with their points.' },\n last: { type: 'integer', minimum: 1, maximum: 1000, description: 'Only the most recent n strokes (default 100).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'List recorded strokes', ...READ },\n handler: ({ ids, last }) => {\n const all = c.getStrokes();\n const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(last ?? 100));\n if (ids) for (const id of ids) if (!all.some((s) => s.id === id)) throw new Error(`Unknown stroke \"${id}\". Call list_strokes without ids for the ids.`);\n return {\n total: all.length,\n strokes: picked.map((s) => {\n const xs = s.points.map((p) => p.x);\n const ys = s.points.map((p) => p.y);\n const pad = s.brush.tip.size / 2;\n return {\n id: s.id,\n layerId: s.layerId,\n brushId: s.brush.id,\n brushName: s.brush.name,\n color: s.color,\n points: ids ? s.points : s.points.length,\n seed: s.seed,\n copies: s.copies?.length ?? 0,\n bounds: { x: Math.floor(Math.min(...xs) - pad), y: Math.floor(Math.min(...ys) - pad), width: Math.ceil(Math.max(...xs) - Math.min(...xs) + 2 * pad), height: Math.ceil(Math.max(...ys) - Math.min(...ys) + 2 * pad) },\n };\n }),\n };\n },\n });\n\n add({\n name: 'replay_strokes',\n title: 'Replay strokes, varied',\n description:\n 'Paints recorded strokes again, moved, scaled, rotated, flipped, recoloured, with another brush or on another layer, repeat times (each repeat applies the variation again: a row, a fan, a spiral). Strokes replay exactly (same seed). All of it is one undo step. Returns the new stroke ids, the changed bounds and layer ids.',\n inputSchema: {\n type: 'object',\n properties: {\n strokeIds: { type: 'array', minItems: 1, maxItems: 500, items: { type: 'string', description: 'Stroke id.' }, description: 'Stroke ids from list_strokes.' },\n dx: { type: 'number', description: 'Move right by this many pixels per repeat (default 0).' },\n dy: { type: 'number', description: 'Move down by this many pixels per repeat (default 0).' },\n scale: { type: 'number', exclusiveMinimum: 0, maximum: 20, description: 'Scale per repeat about the origin; brush size scales too (default 1).' },\n rotate: { type: 'number', minimum: -360, maximum: 360, description: 'Rotate per repeat about the origin, degrees clockwise (default 0).' },\n flipX: { type: 'boolean', description: 'Mirror left to right about the origin (default false).' },\n origin: {\n type: 'object',\n description: 'Centre for scale, rotate and flip (default the centre of the strokes).',\n properties: { x: { type: 'number', description: 'Document x.' }, y: { type: 'number', description: 'Document y.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n pressureScale: { type: 'number', minimum: 0, maximum: 4, description: 'Multiply every pressure (default 1).' },\n color: { ...COLOR, description: 'Recolour (default each stroke’s colour).' },\n brushId: { type: 'string', description: 'Replay with this brush from list_brushes (default each stroke’s brush).' },\n layerId: { type: 'string', description: 'Paint on this layer (default each stroke’s layer).' },\n repeat: { type: 'integer', minimum: 1, maximum: 100, description: 'How many copies (default 1).' },\n },\n required: ['strokeIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Replay strokes, varied', ...WRITE },\n handler: (a) => {\n const replay = a as Parameters<PaintEditorController['replayStrokes']>[0];\n return act({ kind: 'replay', replay }, () => c.replayStrokes(replay));\n },\n });\n\n // --- View, hand, history -------------------------------------------------------\n\n add({\n name: 'set_view',\n title: 'Rotate, mirror or zoom the view',\n description: 'Changes the view only (the pixels never resample): zoom, rotation and mirroring, as an artist turns the paper or checks the drawing in a mirror. Editor state, not an undo step. Returns the view.',\n inputSchema: {\n type: 'object',\n properties: {\n zoom: { type: 'number', minimum: MIN_ZOOM, maximum: MAX_ZOOM, description: 'Screen pixels per document pixel.' },\n rotation: { type: 'number', minimum: -360, maximum: 360, description: 'Rotation in degrees clockwise.' },\n mirror: { type: 'boolean', description: 'Mirror left to right.' },\n reset: { type: 'boolean', description: 'Back to 100 %, no rotation, no mirror, centred (applied first).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Rotate, mirror or zoom the view', ...SET },\n handler: ({ reset, ...v }) => {\n if (reset) c.resetView();\n return c.setView(v);\n },\n });\n\n add({\n name: 'set_hand',\n title: 'Set the drawing hand',\n description: 'Says which hand holds the pen; palm rejection ignores touches on that side of the pen. Editor state, not an undo step. Returns the hand.',\n inputSchema: { type: 'object', properties: { hand: { type: 'string', enum: ['right', 'left'], description: 'right or left.' } }, required: ['hand'], additionalProperties: false },\n annotations: { title: 'Set the drawing hand', ...SET },\n handler: ({ hand }) => ({ hand: c.setHand(hand) }),\n });\n\n add({\n name: 'undo_paint',\n title: 'Undo',\n description: 'Undoes the last step (a stroke, a fill, a layer or paper change). Returns the label undone (null when there was nothing) and what can be undone and redone next.',\n inputSchema: EMPTY,\n annotations: { title: 'Undo', ...WRITE },\n handler: () => {\n const undone = c.undo();\n const s = c.getState();\n return { undone, nextUndo: s.undoLabel, nextRedo: s.redoLabel };\n },\n });\n\n add({\n name: 'redo_paint',\n title: 'Redo',\n description: 'Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.',\n inputSchema: EMPTY,\n annotations: { title: 'Redo', ...WRITE },\n handler: () => {\n const redone = c.redo();\n const s = c.getState();\n return { redone, nextUndo: s.undoLabel, nextRedo: s.redoLabel };\n },\n });\n\n // --- Output ------------------------------------------------------------------\n\n add({\n name: 'render_preview',\n title: 'Render a preview',\n description: 'Renders the painting (with its paper unless paper is false) or some layers as a PNG whose longest side is at most maxSide. Changes nothing. Look at it after each block of work.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerIds: { type: 'array', items: { type: 'string', description: 'Layer id.' }, description: 'Only these layers (default all).' },\n paper: { type: 'boolean', description: 'Include the paper (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Render a preview', ...READ },\n handler: ({ maxSide, layerIds, paper }) => {\n if (layerIds) for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const p = c.previewPngBase64({ maxSide: maxSide ?? 512, layerIds, paper });\n return { content: [{ type: 'image', data: p.data, mimeType: 'image/png' }, { type: 'text', text: `Preview ${p.width}×${p.height}` }] };\n },\n });\n\n add({\n name: 'export_image',\n title: 'Export the painting',\n description: 'Exports the painting as PNG, JPEG or WebP (base64). JPEG always includes the paper; JPEG and WebP need a browser canvas. Changes nothing. Returns the data, its MIME type and size.',\n inputSchema: {\n type: 'object',\n properties: {\n format: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp'], description: 'File type (default image/png).' },\n quality: { type: 'number', minimum: 0, maximum: 1, description: 'JPEG / WebP quality 0..1 (default 0.92).' },\n scale: { type: 'number', exclusiveMinimum: 0, maximum: 1, description: 'Shrink by this factor (default 1).' },\n paper: { type: 'boolean', description: 'Include the paper (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Export the painting', ...READ },\n handler: async ({ format, quality, scale, paper }) => {\n const bytes = await c.exportImage({ format, quality, scale, paper });\n return { mimeType: format ?? 'image/png', bytes: bytes.length, data: bytesToBase64(bytes) };\n },\n });\n\n add({\n name: 'export_psd',\n title: 'Export as PSD',\n description:\n 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked \"Paper\" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',\n inputSchema: EMPTY,\n annotations: { title: 'Export as PSD', ...READ },\n handler: () => {\n const r = c.exportPsd();\n return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };\n },\n });\n\n add({\n name: 'save_document',\n title: 'Save the document',\n description: 'Returns the document as JSON (format pixerate-paint, version 1): canvas, paper, layers with PNG data URLs, and the recorded strokes unless strokes is false. Changes nothing.',\n inputSchema: { type: 'object', properties: { strokes: { type: 'boolean', description: 'Include the recorded strokes (default true).' } }, additionalProperties: false },\n annotations: { title: 'Save the document', ...READ },\n handler: ({ strokes }) => c.toJSON({ strokes }),\n });\n\n add({\n name: 'open_document',\n title: 'Open a document',\n description: 'Opens a saved paint document (as save_document returns), replacing the painting and clearing the undo history. Returns the canvas, the layers and anything that could not be read.',\n inputSchema: { type: 'object', properties: { document: { type: 'object', description: 'The saved document JSON.' } }, required: ['document'], additionalProperties: false },\n annotations: { title: 'Open a document', ...DESTRUCTIVE },\n handler: ({ document }) => {\n const problems = c.loadJSON(document);\n const d = c.document;\n return { width: d.canvas.width, height: d.canvas.height, layers: c.listLayers(), strokes: d.strokes.length, problems };\n },\n });\n\n tools.push(...createPaintGuideTools(c));\n tools.push(...createPaintWetTools(c));\n tools.push(...createPaintThickTools(c));\n tools.push(...createPaintReplayTools(c));\n tools.push(...createPaintStickerTools(c));\n tools.push(...createPigmentTools());\n\n return { id: options.id ?? 'paint-editor', namespace: options.namespace, tools };\n}\n","/**\n * Framework-free UI logic shared by the React and Svelte paint editors, so both behave the same:\n *\n * - `PaintGesture` turns pointer events (structurally typed, no DOM import) into strokes (with\n * coalesced samples and a predicted tail), colour picks, pans, view rotation and zoom, with palm\n * rejection while a pen is down.\n * - `PaintCanvasPresenter` keeps a document-sized 2D canvas up to date from the compositor,\n * uploading only the tiles that changed.\n * - Guide overlays as line segments, and the slider tables of the brush and paper panels.\n */\n\nimport type { MixerLoad } from '../paint/exchange';\nimport { BLEND_MODES, type BlendModeInfo } from '../raster/blend';\nimport type { ImpastoTool, PaintBrush, PaintBrushInput, WetMediumKind, WetTool } from './brushes';\nimport { TILE, type RasterRect } from '../raster/surface';\nimport type { PaintEditorController } from './controller';\nimport type { PenPoint } from './engine';\nimport { perspectiveGuideLines, symmetryAxisSegments, type Point2 } from './guides';\nimport { stencilOutline, type StencilInput } from './stencils';\nimport type { ParticleKind, ParticleRender } from './procedural';\nimport type { StampKind } from './stickers';\nimport type { WetEngineInfo, WetGpuMode } from './wet';\nimport { isPalmTouch, pointerToPenPoint, type PointerSample } from './input';\nimport { MAX_ZOOM, MIN_ZOOM, screenToDocument } from './view';\nimport { hsvToRgb, parseHexColor, rgbToHexColor, rgbToHsv, wheelToHue, type ColorWheel } from './color';\n\n/** The Pointer Event fields the gesture reads. */\nexport interface PaintPointerEvent extends PointerSample {\n pointerId: number;\n clientX: number;\n clientY: number;\n button?: number;\n buttons?: number;\n shiftKey?: boolean;\n altKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n getCoalescedEvents?(): PaintPointerEvent[];\n getPredictedEvents?(): PaintPointerEvent[];\n}\n\nexport type PaintTool = 'brush' | 'eyedropper' | 'pan' | 'rotate';\n\nexport const PAINT_TOOLS: ReadonlyArray<{ id: PaintTool; label: string; key: string }> = [\n { id: 'brush', label: 'Brush', key: 'b' },\n { id: 'eyedropper', label: 'Eyedropper', key: 'i' },\n { id: 'pan', label: 'Pan', key: 'h' },\n { id: 'rotate', label: 'Rotate view', key: 'r' },\n];\n\n/** Where the viewport element is on screen and how big it is (CSS pixels). */\nexport interface ViewportBox {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\ntype Mode = { kind: 'stroke'; pointerId: number } | { kind: 'pan'; pointerId: number; x: number; y: number; pan: Point2 } | { kind: 'rotate'; pointerId: number; angle: number; rotation: number } | null;\n\nexport class PaintGesture {\n tool: PaintTool = 'brush';\n /** The last refusal (a hidden or locked layer), for the editor to show. */\n error: string | null = null;\n /** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */\n predicted: PenPoint[] = [];\n private mode: Mode = null;\n private penAt: Point2 | null = null;\n private penDown = false;\n\n constructor(\n private readonly controller: PaintEditorController,\n private readonly box: () => ViewportBox\n ) {}\n\n private doc(e: { clientX: number; clientY: number }): Point2 {\n const b = this.box();\n const s = this.controller.getState();\n return screenToDocument(s.view, s.document.canvas, b, e.clientX - b.left, e.clientY - b.top);\n }\n\n private pen(e: PaintPointerEvent): PenPoint {\n const p = this.doc(e);\n return pointerToPenPoint(e, p.x, p.y, this.controller.getState().view);\n }\n\n /** Pointer down; returns true when the gesture took the pointer (capture it). */\n down(e: PaintPointerEvent): boolean {\n this.error = null;\n if (this.mode) return false;\n if (e.pointerType === 'pen') {\n this.penDown = true;\n this.penAt = { x: e.clientX, y: e.clientY };\n } else if (e.pointerType === 'touch' && (this.penDown || isPalmTouch({ x: e.clientX, y: e.clientY }, this.penAt, this.controller.getState().hand))) {\n return false;\n }\n const middle = e.button === 1;\n const tool = middle ? 'pan' : this.tool;\n if (tool === 'pan') {\n this.mode = { kind: 'pan', pointerId: e.pointerId, x: e.clientX, y: e.clientY, pan: { ...this.controller.getState().view.pan } };\n return true;\n }\n if (tool === 'rotate') {\n const b = this.box();\n this.mode = { kind: 'rotate', pointerId: e.pointerId, angle: Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2), rotation: this.controller.getState().view.rotation };\n return true;\n }\n if (tool === 'eyedropper' || e.altKey) {\n this.pick(e);\n return false;\n }\n try {\n const p = this.pen(e);\n const session = this.controller.beginStroke({ start: p });\n session.push([p]);\n this.mode = { kind: 'stroke', pointerId: e.pointerId };\n return true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n return false;\n }\n }\n\n private pick(e: PaintPointerEvent): void {\n const p = this.doc(e);\n const { width, height } = this.controller.document.canvas;\n if (p.x < 0 || p.y < 0 || p.x >= width || p.y >= height) return;\n this.controller.setColor(this.controller.pickColor(p.x, p.y).color);\n }\n\n move(e: PaintPointerEvent): void {\n if (e.pointerType === 'pen') this.penAt = { x: e.clientX, y: e.clientY };\n const m = this.mode;\n if (!m || m.pointerId !== e.pointerId) return;\n if (m.kind === 'pan') {\n this.controller.setView({ pan: { x: m.pan.x + e.clientX - m.x, y: m.pan.y + e.clientY - m.y } });\n return;\n }\n if (m.kind === 'rotate') {\n const b = this.box();\n const a = Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2);\n let r = m.rotation + ((a - m.angle) * 180) / Math.PI;\n if (e.shiftKey) r = Math.round(r / 15) * 15;\n this.controller.setView({ rotation: r });\n return;\n }\n const session = this.controller.activeStroke;\n if (!session) return;\n const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];\n session.push((events.length ? events : [e]).map((ev) => this.pen(ev)));\n const predicted = typeof e.getPredictedEvents === 'function' ? e.getPredictedEvents() : [];\n this.predicted = predicted.map((ev) => this.pen(ev));\n }\n\n up(e: PaintPointerEvent): void {\n if (e.pointerType === 'pen') this.penDown = false;\n const m = this.mode;\n if (!m || m.pointerId !== e.pointerId) return;\n this.mode = null;\n this.predicted = [];\n if (m.kind === 'stroke') this.controller.activeStroke?.end();\n }\n\n cancel(): void {\n this.penDown = false;\n this.predicted = [];\n if (this.mode?.kind === 'stroke') this.controller.activeStroke?.cancel();\n this.mode = null;\n }\n\n get active(): boolean {\n return this.mode !== null;\n }\n\n /** Wheel: zoom about the pointer (Ctrl / pinch or plain wheel), Shift+wheel rotates the view. */\n wheel(e: { clientX: number; clientY: number; deltaY: number; shiftKey?: boolean }): void {\n const s = this.controller.getState();\n if (e.shiftKey) {\n this.controller.setView({ rotation: s.view.rotation + (e.deltaY > 0 ? 5 : -5) });\n return;\n }\n const b = this.box();\n const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, s.view.zoom * Math.exp(-e.deltaY * 0.0015)));\n const k = zoom / s.view.zoom;\n // Keep the point under the pointer still: the pan scales about it.\n const px = e.clientX - b.left - b.width / 2;\n const py = e.clientY - b.top - b.height / 2;\n this.controller.setView({ zoom, pan: { x: px - (px - s.view.pan.x) * k, y: py - (py - s.view.pan.y) * k } });\n }\n}\n\n/** Keyboard shortcuts shared by both editors; returns true when it handled the key. */\nexport function handlePaintShortcut(\n controller: PaintEditorController,\n gesture: PaintGesture,\n e: { key: string; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; target?: unknown }\n): boolean {\n const mod = e.ctrlKey || e.metaKey;\n const key = e.key.toLowerCase();\n if (mod && key === 'z') {\n if (controller.getState().stroking) return true;\n if (e.shiftKey) controller.redo();\n else controller.undo();\n return true;\n }\n if (mod && key === 'y') {\n controller.redo();\n return true;\n }\n if (mod) return false;\n const tool = PAINT_TOOLS.find((t) => t.key === key);\n if (tool) {\n gesture.tool = tool.id;\n return true;\n }\n const brush = controller.getState().brush;\n if (key === '[' || key === ']') {\n const step = Math.max(1, Math.round(brush.tip.size * 0.15));\n controller.setBrush({ tip: { size: Math.max(1, brush.tip.size + (key === ']' ? step : -step)) } });\n return true;\n }\n if (key === 'x') {\n controller.swapColors();\n return true;\n }\n if (key === 'm') {\n controller.setView({ mirror: !controller.getState().view.mirror });\n return true;\n }\n if (key === 'escape') {\n controller.resetView();\n return true;\n }\n return false;\n}\n\n/** The 2D context methods the presenter uses (a `CanvasRenderingContext2D`). */\nexport interface Paint2DContext {\n createImageData(width: number, height: number): { data: Uint8ClampedArray };\n putImageData(image: never, dx: number, dy: number, sx?: number, sy?: number, sw?: number, sh?: number): void;\n}\n\n/** Keeps a document-sized canvas in step with the controller, tile by tile. */\nexport class PaintCanvasPresenter {\n private readonly uploaded = new Map<string, Uint8ClampedArray>();\n private scratch: { data: Uint8ClampedArray } | null = null;\n private size = '';\n\n constructor(private readonly controller: PaintEditorController) {}\n\n /** Draws what changed into `ctx` (a canvas the size of the document); returns the tiles drawn. */\n render(ctx: Paint2DContext): number {\n const { width, height } = this.controller.document.canvas;\n const size = `${width}x${height}`;\n let dirty = this.controller.takeDirty();\n if (size !== this.size) {\n this.size = size;\n this.uploaded.clear();\n dirty = 'all';\n }\n let drawn = 0;\n const rect: RasterRect = { x: 0, y: 0, width, height };\n this.controller.compositeTiles(rect, (tx, ty, data) => {\n const key = `${tx},${ty}`;\n if (this.uploaded.get(key) === data) return;\n this.uploaded.set(key, data);\n if (!this.scratch) this.scratch = ctx.createImageData(TILE, TILE);\n this.scratch.data.set(data);\n const ox = tx * TILE;\n const oy = ty * TILE;\n ctx.putImageData(this.scratch as never, ox, oy, 0, 0, Math.min(TILE, width - ox), Math.min(TILE, height - oy));\n drawn++;\n }, { dirty });\n return drawn;\n }\n}\n\n/** Lines to draw over the canvas for the guides, in document pixels, with a style key. */\nexport function guideOverlay(controller: PaintEditorController): Array<{ from: Point2; to: Point2; kind: 'symmetry' | 'perspective' | 'snap' | 'stencil' }> {\n const s = controller.getState();\n const size = s.document.canvas;\n const out: Array<{ from: Point2; to: Point2; kind: 'symmetry' | 'perspective' | 'snap' | 'stencil' }> = [];\n for (const st of s.guides.stencils) {\n if (!st.enabled) continue;\n const line = stencilOutline(st);\n for (let i = 0; i + 1 < line.length; i++) out.push({ from: line[i], to: line[i + 1], kind: 'stencil' });\n }\n for (const [from, to] of symmetryAxisSegments(s.guides.symmetry, size)) out.push({ from, to, kind: 'symmetry' });\n if (s.guides.perspective.enabled) for (const [from, to] of perspectiveGuideLines(s.guides.perspective, size, 16)) out.push({ from, to, kind: 'perspective' });\n return out;\n}\n\n/** One slider of a panel: a path into the brush (or paper) settings. */\nexport interface SliderControl {\n path: string;\n label: string;\n min: number;\n max: number;\n step: number;\n}\n\nexport const BRUSH_SLIDERS: readonly SliderControl[] = [\n { path: 'tip.size', label: 'Size', min: 1, max: 400, step: 1 },\n { path: 'tip.opacity', label: 'Opacity', min: 0, max: 1, step: 0.01 },\n { path: 'tip.flow', label: 'Flow', min: 0, max: 1, step: 0.01 },\n { path: 'tip.hardness', label: 'Hardness', min: 0, max: 1, step: 0.01 },\n { path: 'tip.spacing', label: 'Spacing', min: 0.01, max: 1, step: 0.01 },\n { path: 'tip.roundness', label: 'Roundness', min: 0.05, max: 1, step: 0.01 },\n { path: 'tip.angle', label: 'Angle', min: -180, max: 180, step: 1 },\n { path: 'grain', label: 'Grain', min: 0, max: 1, step: 0.01 },\n { path: 'tip.smoothing', label: 'Smoothing', min: 0, max: 0.95, step: 0.01 },\n { path: 'pressureSmoothing', label: 'Pressure smoothing', min: 0, max: 0.95, step: 0.01 },\n { path: 'dynamics.minSize', label: 'Min size', min: 0, max: 1, step: 0.01 },\n { path: 'dynamics.tiltSize', label: 'Tilt size', min: 0, max: 1, step: 0.01 },\n { path: 'dynamics.velocitySize', label: 'Speed size', min: -1, max: 1, step: 0.01 },\n];\n\nexport const PAPER_SLIDERS: readonly SliderControl[] = [\n { path: 'depth', label: 'Grain depth', min: 0, max: 1, step: 0.01 },\n { path: 'scale', label: 'Grain scale', min: 0.25, max: 8, step: 0.05 },\n { path: 'shading', label: 'Shading', min: 0, max: 1, step: 0.01 },\n { path: 'dry.contrast', label: 'Contrast', min: 0, max: 3, step: 0.05 },\n { path: 'absorption', label: 'Absorption', min: 0, max: 1, step: 0.01 },\n { path: 'permeability', label: 'Permeability', min: 0, max: 1, step: 0.01 },\n { path: 'roughness', label: 'Roughness', min: 0, max: 1, step: 0.01 },\n { path: 'metallic', label: 'Metallic', min: 0, max: 1, step: 0.01 },\n];\n\n/** Reads `a.b` from an object. */\nexport function readPath(obj: unknown, path: string): number {\n let v: unknown = obj;\n for (const k of path.split('.')) v = (v as Record<string, unknown>)?.[k];\n return typeof v === 'number' ? v : 0;\n}\n\n/** `{a: {b: value}}` for `a.b`, keeping the rest of `obj`'s `a`. */\nexport function patchPath(obj: Record<string, unknown>, path: string, value: number): Record<string, unknown> {\n const [head, ...rest] = path.split('.');\n if (!rest.length) return { [head]: value };\n return { [head]: { ...(obj[head] as Record<string, unknown>), ...patchPath((obj[head] as Record<string, unknown>) ?? {}, rest.join('.'), value) } };\n}\n\n/** The point on a colour wheel of `radius` (centre 0, 0) for a wheel angle in degrees, red at the top. */\nexport function wheelPoint(angle: number, radius: number): Point2 {\n const a = ((angle - 90) * Math.PI) / 180;\n return { x: Math.cos(a) * radius, y: Math.sin(a) * radius };\n}\n\n/** The wheel angle (degrees, red at the top, clockwise) of a point relative to the wheel centre. */\nexport function wheelAngle(x: number, y: number): number {\n return ((Math.atan2(y, x) * 180) / Math.PI + 90 + 360) % 360;\n}\n\n/**\n * A hue ring for a colour wheel, `size × size` RGBA: hues at full saturation and value between\n * `inner` and 1 of the radius, red at the top, laid out on the RYB or RGB wheel.\n */\nexport function renderColorWheel(size: number, wheel: ColorWheel, inner = 0.64): { width: number; height: number; data: Uint8ClampedArray } {\n const data = new Uint8ClampedArray(size * size * 4);\n const r = size / 2;\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n const dx = x + 0.5 - r;\n const dy = y + 0.5 - r;\n const d = Math.hypot(dx, dy) / r;\n if (d > 1 || d < inner) continue;\n const [cr, cg, cb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), 1, 1);\n const i = (y * size + x) * 4;\n // Anti-aliased edges.\n const edge = Math.min(1, (1 - d) * r, (d - inner) * r);\n data[i] = cr;\n data[i + 1] = cg;\n data[i + 2] = cb;\n data[i + 3] = Math.round(255 * Math.max(0, edge));\n }\n }\n return { width: size, height: size, data };\n}\n\n/** The colour for a click on the wheel ring at (dx, dy) from its centre, keeping `color`'s saturation and value. */\nexport function wheelPick(dx: number, dy: number, wheel: ColorWheel, color: string): string {\n const [r, g, b] = parseHexColor(color);\n const { s, v } = rgbToHsv(r, g, b);\n const [nr, ng, nb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), s || 1, v || 1);\n return rgbToHexColor(nr, ng, nb);\n}\n\n/** A group of the blend menu: the Photoshop modes, then pigment and the painterly modes. */\nexport interface BlendMenuGroup {\n label: string;\n modes: readonly BlendModeInfo[];\n}\n\n/**\n * The layer and brush blend menus as groups: the 26 standard modes, then the paint modes\n * (`pigment` and the painterly `watercolor`, `tint`, `highlight`, `shadow`), each with its\n * description as a tooltip. Both bindings render this as `<optgroup>`s.\n */\nexport const BLEND_MENU_GROUPS: readonly BlendMenuGroup[] = [\n { label: 'Standard', modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== 'pigment') },\n { label: 'Paint', modes: BLEND_MODES.filter((m) => m.painterly || m.id === 'pigment') },\n];\n\n/** The engine-specific part of the brush panel: which sliders and toggles the active brush has. */\nexport interface BrushEngineControls {\n /** Section heading. */\n title: string;\n sliders: readonly SliderControl[];\n /** Where the RGB / pigment choice is stored, or `null` when the engine has none. */\n mixingPath: 'behavior.mixing' | 'blender.mixing' | 'smudge.mixing' | null;\n /** The dirty-brush toggle (`behavior.dirty`). */\n dirty: boolean;\n /** The multi-colour controls (`behavior.multicolor`; the panels show the brush gradient, `brush.paint`, instead). */\n multicolor: boolean;\n /** Load / Clean buttons and a swatch of what the brush carries. */\n load: boolean;\n /** The wet tool menu (`wet.tool`, `WET_TOOLS`) and medium menu (`wet.medium`, `WET_MEDIA`). */\n wetTool?: boolean;\n /** The impasto tool menu (`impasto.tool`, `IMPASTO_TOOLS`). */\n impastoTool?: boolean;\n /** The particle kind and render menus (`particle.kind`, `particle.render`). */\n particleMenus?: boolean;\n /** The sticker stamp toggles (`sticker.stamps`, `STAMP_KINDS`). */\n stickerStamps?: boolean;\n}\n\nconst PICKUP_SLIDERS: readonly SliderControl[] = [\n { path: 'behavior.pickup', label: 'Pick-up', min: 0, max: 1, step: 0.01 },\n { path: 'behavior.mix', label: 'Mix', min: 0, max: 1, step: 0.01 },\n];\n\n/**\n * The controls the active brush's engine adds to the brush panel (`null` for the eraser):\n * blender strength and blend distance; smudge strength and length; the Mixer Brush's pick-up,\n * mix and load; a dab brush's pick-up and mix ratios. Every engine but the eraser has an RGB /\n * pigment choice; dab and mixer brushes also have the dirty and multi-colour controls. Pick-up\n * and mix always go through `behavior`, which keeps a Mixer Brush's `mixer` block in sync.\n */\nexport function brushEngineControls(brush: Pick<PaintBrush, 'engine'> & { wet?: PaintBrush['wet'] }): BrushEngineControls | null {\n switch (brush.engine) {\n case 'particle':\n return {\n title: 'Particles',\n sliders: [\n { path: 'particle.count', label: 'Particles', min: 1, max: 64, step: 1 },\n { path: 'particle.size', label: 'Mark size', min: 0, max: 1, step: 0.01 },\n { path: 'particle.spread', label: 'Spread', min: 0, max: 1, step: 0.01 },\n { path: 'particle.stiffness', label: 'Stiffness', min: 0, max: 1, step: 0.01 },\n { path: 'particle.damping', label: 'Damping', min: 0, max: 1, step: 0.01 },\n { path: 'particle.speed', label: 'Speed', min: 0, max: 1, step: 0.01 },\n { path: 'particle.gravity', label: 'Gravity', min: -1, max: 1, step: 0.01 },\n { path: 'particle.life', label: 'Life', min: 0, max: 1, step: 0.01 },\n { path: 'particle.interval', label: 'Bead spacing', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: null,\n dirty: false,\n multicolor: false,\n load: false,\n particleMenus: true,\n };\n case 'bristle':\n return {\n title: 'Bristles',\n sliders: [\n { path: 'bristle.count', label: 'Bristles', min: 3, max: 128, step: 1 },\n { path: 'bristle.stiffness', label: 'Stiffness', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.splay', label: 'Splay', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.thickness', label: 'Bristle width', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.load', label: 'Load', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.jitter', label: 'Variation', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: null,\n dirty: false,\n multicolor: false,\n load: false,\n };\n case 'sticker':\n return {\n title: 'Stickers',\n sliders: [\n { path: 'sticker.density', label: 'Density', min: 0, max: 1, step: 0.01 },\n { path: 'sticker.sizeJitter', label: 'Size jitter', min: 0, max: 1, step: 0.01 },\n { path: 'sticker.rotationJitter', label: 'Turn jitter', min: 0, max: 1, step: 0.01 },\n { path: 'sticker.scatter', label: 'Scatter', min: 0, max: 2, step: 0.01 },\n { path: 'sticker.colorJitter', label: 'Colour jitter', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: null,\n dirty: false,\n multicolor: false,\n load: false,\n stickerStamps: true,\n };\n case 'impasto':\n return {\n title: 'Thick paint',\n sliders: [\n { path: 'impasto.thickness', label: 'Thickness', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.gloss', label: 'Gloss', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.metallic', label: 'Metallic', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.load', label: 'Load', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.thinner', label: 'Thinner', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.drag', label: 'Drag', min: 0, max: 1, step: 0.01 },\n ...PICKUP_SLIDERS,\n ],\n mixingPath: 'behavior.mixing',\n dirty: true,\n multicolor: false,\n load: true,\n impastoTool: true,\n };\n case 'wet': {\n const tool = brush.wet?.tool ?? 'paint';\n const sliders: SliderControl[] =\n tool === 'paint'\n ? [\n { path: 'wet.water', label: 'Water', min: 0, max: 1, step: 0.01 },\n { path: 'wet.pigment', label: 'Pigment', min: 0, max: 1, step: 0.01 },\n { path: 'wet.body', label: 'Body (opacity)', min: 0, max: 1, step: 0.01 },\n { path: 'wet.rewet', label: 'Re-wet', min: 0, max: 1, step: 0.01 },\n { path: 'wet.load', label: 'Load', min: 0, max: 1, step: 0.01 },\n { path: 'wet.thinner', label: 'Thinner', min: 0, max: 1, step: 0.01 },\n ...PICKUP_SLIDERS,\n ]\n : tool === 'water'\n ? [\n { path: 'wet.water', label: 'Water', min: 0, max: 1, step: 0.01 },\n { path: 'wet.rewet', label: 'Re-wet', min: 0, max: 1, step: 0.01 },\n ]\n : [{ path: 'wet.strength', label: 'Strength', min: 0, max: 1, step: 0.01 }];\n return { title: 'Wet brush', sliders, mixingPath: tool === 'paint' ? 'behavior.mixing' : null, dirty: tool === 'paint', multicolor: false, load: tool === 'paint', wetTool: true };\n }\n case 'blender':\n return {\n title: 'Blender',\n sliders: [\n { path: 'blender.strength', label: 'Strength', min: 0, max: 1, step: 0.01 },\n { path: 'blender.distance', label: 'Blend distance', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: 'blender.mixing',\n dirty: false,\n multicolor: false,\n load: false,\n };\n case 'smudge':\n return {\n title: 'Smudge',\n sliders: [\n { path: 'smudge.strength', label: 'Strength', min: 0, max: 1, step: 0.01 },\n { path: 'smudge.length', label: 'Length', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: 'smudge.mixing',\n dirty: false,\n multicolor: false,\n load: false,\n };\n case 'mixer':\n return { title: 'Mixer Brush', sliders: [...PICKUP_SLIDERS, { path: 'mixer.load', label: 'Load', min: 0, max: 1, step: 0.01 }], mixingPath: 'behavior.mixing', dirty: true, multicolor: true, load: true };\n case 'dab':\n return { title: 'Paint behaviour', sliders: PICKUP_SLIDERS, mixingPath: 'behavior.mixing', dirty: true, multicolor: true, load: true };\n default:\n return null;\n }\n}\n\n/** The multi-colour sliders for a mode: softness, and dabs per colour in `dab` mode. */\nexport function multiColorSliders(mode: string): readonly SliderControl[] {\n if (mode === 'off') return [];\n const soft: SliderControl = { path: 'behavior.multicolor.softness', label: 'Colour softness', min: 0, max: 1, step: 0.01 };\n return mode === 'dab' ? [soft, { path: 'behavior.multicolor.length', label: 'Dabs per colour', min: 1, max: 100, step: 1 }] : [soft];\n}\n\n/** A brush change setting `path` (e.g. `smudge.mixing`, `behavior.dirty`) to any value, keeping the rest of its block. */\nexport function patchBrushValue(brush: PaintBrush, path: string, value: unknown): PaintBrushInput {\n const set = (obj: Record<string, unknown>, keys: string[]): Record<string, unknown> => {\n const [head, ...rest] = keys;\n if (!rest.length) return { ...obj, [head]: value };\n return { ...obj, [head]: set((obj[head] as Record<string, unknown>) ?? {}, rest) };\n };\n const [head, ...rest] = path.split('.');\n if (!rest.length) return { [head]: value } as PaintBrushInput;\n return { [head]: set({ ...((brush as unknown as Record<string, Record<string, unknown>>)[head] ?? {}) }, rest) } as PaintBrushInput;\n}\n\n/** What the brush carries (picked-up paint, else loaded paint) as #rrggbb, or `null` when it is clean and empty. */\nexport function brushLoadColor(load: MixerLoad): string | null {\n const c = load.pickup ?? load.reservoir;\n return c ? rgbToHexColor(Math.round(c[0] * 255), Math.round(c[1] * 255), Math.round(c[2] * 255)) : null;\n}\n\n/** Sample sizes (square, in pad pixels) for picking from the mixing pad. */\nexport const PAD_SAMPLE_SIZES: readonly number[] = [1, 3, 5, 9, 15];\n\n/** The averaging radius `mixingPadPick` takes for a sample size: 1 → 0, 3 → 1, 5 → 2 … */\nexport function padSampleRadius(size: number): number {\n return Math.max(0, Math.floor((size - 1) / 2));\n}\n\n// ---------------------------------------------------------------------------\n// Wet media\n// ---------------------------------------------------------------------------\n\n/** Labels of the wet tools, for the wet brush's tool menu. */\nexport const WET_TOOL_LABELS: Readonly<Record<WetTool, string>> = {\n paint: 'Paint',\n water: 'Add water',\n dry: 'Dry',\n blow: 'Blow',\n blend: 'Blend',\n smudge: 'Smudge',\n blot: 'Blot (erase wet)',\n};\n\n/** The wet panel's sliders for a layer's wet settings (paths into `WetParams`). */\nexport const WET_PARAM_SLIDERS: readonly SliderControl[] = [\n { path: 'viscosity', label: 'Viscosity', min: 0, max: 1, step: 0.01 },\n { path: 'evaporation', label: 'Evaporation', min: 0, max: 1, step: 0.01 },\n { path: 'edgeDarkening', label: 'Edge darkening', min: 0, max: 1, step: 0.01 },\n { path: 'granulation', label: 'Granulation', min: 0, max: 1, step: 0.01 },\n { path: 'bloom', label: 'Blooms', min: 0, max: 1, step: 0.01 },\n { path: 'bleed', label: 'Colour bleed', min: 0, max: 1, step: 0.01 },\n { path: 'pigmentWeight', label: 'Pigment weight', min: 0, max: 1, step: 0.01 },\n { path: 'pickup', label: 'Lifting', min: 0, max: 1, step: 0.01 },\n { path: 'fixture', label: 'Staining', min: 0, max: 1, step: 0.01 },\n];\n\n/** The canvas tilt sliders. */\nexport const WET_TILT_SLIDERS: readonly SliderControl[] = [\n { path: 'angle', label: 'Tilt', min: 0, max: 90, step: 1 },\n { path: 'direction', label: 'Downhill', min: 0, max: 359, step: 1 },\n];\n\n/** The speed slider (steps per second). */\nexport const WET_SPEED_SLIDER: SliderControl = { path: 'speed', label: 'Speed', min: 15, max: 480, step: 15 };\n\n/** The wet-engine choices a panel offers (Engine B, the WebGPU accelerator). */\nexport const WET_GPU_OPTIONS: readonly { value: WetGpuMode; label: string }[] = [\n { value: 'auto', label: 'GPU for large washes' },\n { value: 'off', label: 'CPU only' },\n];\n\n/** One line saying what runs the wet paint (for panels). */\nexport function wetEngineSummary(e: WetEngineInfo): string {\n const gpu = e.adapter ? `GPU (${e.adapter})` : 'GPU';\n switch (e.state) {\n case 'off':\n return 'CPU only (GPU off).';\n case 'not-started':\n return 'The GPU starts with the first wet stroke.';\n case 'starting':\n return 'Starting the GPU…';\n case 'active':\n return `${gpu} shading ${e.activeTiles} wet tiles; the CPU keeps the result.`;\n case 'standby':\n return `${gpu} ready; it takes over at ${e.minTiles} wet tiles (${e.activeTiles} now).`;\n default:\n return e.reason ?? 'The GPU is not in use.';\n }\n}\n\n/**\n * Runs the wet paint's clock with the display: `controller.tickWet(now)` every animation frame\n * (it does nothing while nothing is wet or time is paused). Returns a function that stops it.\n * Bindings call it once per mounted canvas.\n */\nexport function startWetClock(controller: PaintEditorController, frame?: (cb: (t: number) => void) => number, cancel?: (id: number) => void): () => void {\n const g = globalThis as { requestAnimationFrame?: (cb: (t: number) => void) => number; cancelAnimationFrame?: (id: number) => void };\n const raf = frame ?? g.requestAnimationFrame;\n const caf = cancel ?? g.cancelAnimationFrame;\n if (!raf) return () => {};\n let stopped = false;\n let id = 0;\n const loop = (t: number) => {\n if (stopped) return;\n controller.tickWet(t).catch(() => {});\n id = raf(loop);\n };\n id = raf(loop);\n return () => {\n stopped = true;\n caf?.(id);\n };\n}\n\n/**\n * Draws the water overlay (blue where the surface is wet, faint where the paper is damp) into a\n * document-sized 2D context, clearing it first. Draws nothing when the overlay is off.\n */\nexport function drawWaterOverlay(controller: PaintEditorController, ctx: Paint2DContext & { clearRect(x: number, y: number, w: number, h: number): void }): number {\n const { width, height } = controller.document.canvas;\n ctx.clearRect(0, 0, width, height);\n if (!controller.getState().wet.overlay) return 0;\n const tiles = controller.waterOverlayTiles({ x: 0, y: 0, width, height });\n if (!tiles.length) return 0;\n const img = ctx.createImageData(TILE, TILE);\n for (const t of tiles) {\n img.data.set(t.data);\n ctx.putImageData(img as never, t.tx * TILE, t.ty * TILE, 0, 0, Math.min(TILE, width - t.tx * TILE), Math.min(TILE, height - t.ty * TILE));\n }\n return tiles.length;\n}\n\n/** Labels of the wet media, for the wet brush's medium menu. */\nexport const WET_MEDIUM_LABELS: Readonly<Record<WetMediumKind, string>> = { watercolor: 'Watercolour', gouache: 'Gouache', oil: 'Oil' };\n\n/** Labels of the impasto tools. */\nexport const IMPASTO_TOOL_LABELS: Readonly<Record<ImpastoTool, string>> = { brush: 'Brush', tube: 'Paint tube', knife: 'Palette knife', roller: 'Roller', gloop: 'Gloop pen' };\n\n/** The light sliders (thick paint and metallic paper). */\nexport const LIGHT_SLIDERS: readonly SliderControl[] = [\n { path: 'azimuth', label: 'Light direction', min: 0, max: 359, step: 1 },\n { path: 'elevation', label: 'Light height', min: 5, max: 90, step: 1 },\n { path: 'relief', label: 'Relief', min: 0, max: 1, step: 0.01 },\n];\n\n/** Labels of the particle kinds and renders. */\nexport const PARTICLE_KIND_LABELS: Readonly<Record<ParticleKind, string>> = { string: 'Strings', spring: 'Springs', orbit: 'Orbits', flow: 'Flow' };\nexport const PARTICLE_RENDER_LABELS: Readonly<Record<ParticleRender, string>> = { dot: 'Dots', line: 'Lines', sequence: 'Beads' };\n\n/** Labels of the sticker stamps. */\nexport const STAMP_LABELS: Readonly<Record<StampKind, string>> = { dot: 'Dot', star: 'Star', heart: 'Heart', leaf: 'Leaf', flower: 'Flower', confetti: 'Confetti', splat: 'Splat', sparkle: 'Sparkle', ring: 'Ring' };\n\n/** Toggles one stamp in a sticker brush's set (never leaving it empty). */\nexport function toggleStamp(stamps: readonly StampKind[], stamp: StampKind): StampKind[] {\n const next = stamps.includes(stamp) ? stamps.filter((s) => s !== stamp) : [...stamps, stamp];\n return next.length ? next : [stamp];\n}\n\n/** The sticker panel's sliders for the selected sticker. */\nexport const STICKER_SLIDERS: readonly SliderControl[] = [\n { path: 'size', label: 'Size', min: 2, max: 400, step: 1 },\n { path: 'rotation', label: 'Rotation', min: -180, max: 180, step: 1 },\n { path: 'opacity', label: 'Opacity', min: 0, max: 1, step: 0.01 },\n];\n\n/** Stencils a toolbar can lay with one click, centred on the canvas. */\nexport function quickStencil(kind: 'ruler' | 'circle' | 'ellipse-mask' | 'edge-mask', canvas: { width: number; height: number }): StencilInput {\n const { width: w, height: h } = canvas;\n switch (kind) {\n case 'ruler':\n return { kind: 'ruler', from: { x: w * 0.1, y: h / 2 }, to: { x: w * 0.9, y: h / 2 } };\n case 'circle':\n return { kind: 'arc', center: { x: w / 2, y: h / 2 }, radius: Math.min(w, h) * 0.3 };\n case 'ellipse-mask':\n return { kind: 'shape', shape: 'ellipse', rect: { x: w * 0.2, y: h * 0.2, width: w * 0.6, height: h * 0.6 } };\n case 'edge-mask':\n return { kind: 'line', from: { x: w / 2, y: 0 }, to: { x: w / 2, y: h } };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/paint-editor/index.cjs","../../src/paint-editor/document.ts","../../src/paint-editor/dab-stroke.ts","../../src/paint-editor/thick.ts","../../src/paint-editor/stroke.ts","../../src/paint-editor/stencils.ts","../../src/paint-editor/strokes.ts","../../src/paint-editor/render.ts","../../src/paint-editor/serialize.ts","../../src/paint-editor/view.ts","../../src/paint-editor/input.ts","../../src/paint-editor/encode.ts","../../src/paint-editor/wet.ts","../../src/paint-editor/thumbnails.ts","../../src/paint-editor/guides.ts","../../src/paint-editor/color.ts","../../src/paint-editor/mixing-pad.ts","../../src/paint-editor/replay.ts","../../src/paint-editor/controller.ts","../../src/paint-editor/presence.ts","../../src/paint-editor/sticker-tools.ts","../../src/paint-editor/guide-tools.ts","../../src/paint-editor/wet-tools.ts","../../src/paint-editor/thick-tools.ts","../../src/paint-editor/replay-tools.ts","../../src/paint-editor/tools.ts","../../src/paint-editor/ui.ts"],"names":["n","raw","d","HEX","layers","EMPTY","LAYER_ID","COLOR","WRITE","head"],"mappings":"AAAA;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;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;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;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACnMO,IAAM,gBAAA,EAAkB,CAAA;AACxB,IAAM,gBAAA,EAAkB,KAAA;AACxB,IAAM,YAAA,EAAc,GAAA;AAqC3B,IAAI,OAAA,EAAS,CAAA;AAGN,SAAS,eAAA,CAAA,EAA0B;AACxC,EAAA,OAAO,CAAA,EAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,QAAA,CAAS,EAAE,CAAC,CAAA,EAAA;AACrC;AAGoD;AAChB,EAAA;AACpC;AAEO;AACwB,EAAA;AACd,IAAA;AACD,IAAA;AACd,EAAA;AACF;AAGwE;AAC3B,EAAA;AACX,IAAA;AACR,IAAA;AACS,IAAA;AACxB,IAAA;AACT,EAAA;AACoC,EAAA;AACJ,EAAA;AACA,EAAA;AAClC;AAGoC;AACD,EAAA;AACF,EAAA;AACP,EAAA;AAC1B;ADwJsC;AACA;AE7Mf;AAGqD;AAC/C,EAAA;AACG,IAAA;AACK,MAAA;AACI,MAAA;AACpC,IAAA;AACH,EAAA;AACkC,EAAA;AACpC;AAGqD;AACzB,EAAA;AACO,EAAA;AACG,EAAA;AACtC;AAGuF;AAC7D,EAAA;AACA,EAAA;AACE,EAAA;AACN,IAAA;AACuB,IAAA;AACP,IAAA;AACV,IAAA;AAC1B,EAAA;AACoC,EAAA;AACtC;AAGkE;AAC9C,EAAA;AACQ,EAAA;AACU,EAAA;AAED,IAAA;AACF,IAAA;AACH,IAAA;AAC9B,EAAA;AACO,EAAA;AACT;AAGyD;AACrC,EAAA;AACS,EAAA;AACS,IAAA;AACF,IAAA;AAClC,EAAA;AACgC,EAAA;AACV,EAAA;AACa,EAAA;AACrC;AAQ4B;AA2BxB,EAAA;AAFS,IAAA;AACQ,IAAA;AAEe,IAAA;AACF,IAAA;AACL,IAAA;AACM,IAAA;AACA,IAAA;AACG,IAAA;AACP,MAAA;AACE,MAAA;AACF,MAAA;AACzB,IAAA;AAE2B,IAAA;AACE,IAAA;AACE,IAAA;AACJ,IAAA;AACD,IAAA;AACM,IAAA;AACJ,IAAA;AACE,IAAA;AAClC,EAAA;AAtBW,EAAA;AACQ,EAAA;AAzBR,iBAAA;AACM,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACyD,kBAAA;AACzD,EAAA;AAAA;AAEA,EAAA;AACoB,kBAAA;AACK,kBAAA;AACzB,EAAA;AACA,EAAA;AACe,kBAAA;AACA,kBAAA;AACE,kBAAA;AACU,kBAAA;AACT,kBAAA;AACA,mBAAA;AAAA;AA4BN,EAAA;AACK,IAAA;AAClC,EAAA;AAEgD,EAAA;AACjB,IAAA;AACH,IAAA;AACT,MAAA;AACiB,MAAA;AACjB,MAAA;AACe,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AAC9B,MAAA;AACH,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGuB,EAAA;AACQ,IAAA;AACZ,IAAA;AACA,IAAA;AACa,IAAA;AACJ,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AAC9B,IAAA;AACM,IAAA;AACT,EAAA;AAE2E,EAAA;AACnD,IAAA;AACS,MAAA;AACD,MAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAGyF,EAAA;AAC3D,IAAA;AACH,IAAA;AACM,MAAA;AACA,MAAA;AAC/B,IAAA;AACkC,IAAA;AACxB,IAAA;AACuB,IAAA;AACJ,IAAA;AACK,IAAA;AACP,IAAA;AACO,IAAA;AACR,MAAA;AACG,MAAA;AAC7B,IAAA;AACkB,IAAA;AACpB,EAAA;AAE6D,EAAA;AACxC,IAAA;AACO,IAAA;AACC,IAAA;AACK,IAAA;AACA,MAAA;AAC9B,MAAA;AACF,IAAA;AAC+B,IAAA;AACI,MAAA;AACjC,MAAA;AACF,IAAA;AACc,IAAA;AAEe,IAAA;AAElB,IAAA;AACe,IAAA;AACM,MAAA;AACC,MAAA;AACtB,MAAA;AACX,IAAA;AACiC,IAAA;AAC7B,IAAA;AACO,IAAA;AACQ,IAAA;AACY,MAAA;AAChB,MAAA;AACkB,MAAA;AACC,IAAA;AACP,IAAA;AACD,MAAA;AACnB,MAAA;AAC0B,QAAA;AACJ,QAAA;AAC3B,MAAA;AACF,IAAA;AAES,IAAA;AACX,EAAA;AAAA;AAGyE,EAAA;AACxD,IAAA;AACE,IAAA;AACL,IAAA;AACkB,IAAA;AACI,MAAA;AACS,MAAA;AACrB,MAAA;AACX,MAAA;AACX,IAAA;AAC4B,IAAA;AACG,IAAA;AAChB,IAAA;AAEgB,IAAA;AACA,IAAA;AACC,IAAA;AACR,IAAA;AACU,IAAA;AACD,MAAA;AACJ,MAAA;AACC,QAAA;AACI,QAAA;AACX,QAAA;AACI,QAAA;AACA,QAAA;AACzB,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG+E,EAAA;AAC9D,IAAA;AACa,IAAA;AACE,IAAA;AACJ,IAAA;AACK,IAAA;AACA,IAAA;AACC,IAAA;AACR,IAAA;AACU,IAAA;AACD,MAAA;AACJ,MAAA;AACC,QAAA;AACG,QAAA;AAC/B,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG2E,EAAA;AACxD,IAAA;AACW,IAAA;AACK,IAAA;AACF,IAAA;AACG,IAAA;AACL,MAAA;AACR,QAAA;AACY,QAAA;AACA,QAAA;AAC/B,MAAA;AACF,IAAA;AAC6B,IAAA;AACJ,IAAA;AACS,IAAA;AACL,MAAA;AACR,QAAA;AACL,UAAA;AACV,UAAA;AACF,QAAA;AACuB,QAAA;AACK,QAAA;AACF,QAAA;AAC5B,MAAA;AACF,IAAA;AACgC,IAAA;AAClC,EAAA;AACF;AF+KsC;AACA;AGre8B;AAEA;AACb,EAAA;AAC9C,EAAA;AAC4B,IAAA;AACA,IAAA;AACD,IAAA;AAClC,EAAA;AACF;AAUoD;AAChB,EAAA;AACpC;AAGkB;AACF;AAGiD;AAC5B,EAAA;AACE,EAAA;AACH,EAAA;AACpC;AAEsD;AAChB,EAAA;AACtC;AAOsF;AAClD,EAAA;AACA,EAAA;AACD,EAAA;AACP,EAAA;AACU,EAAA;AACjB,EAAA;AACA,EAAA;AACA,EAAA;AACiB,EAAA;AACD,EAAA;AACY,EAAA;AACjB,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACd,IAAA;AACY,IAAA;AAC7B,EAAA;AAC+B,EAAA;AACE,IAAA;AACR,MAAA;AACP,MAAA;AACU,MAAA;AACN,MAAA;AACW,MAAA;AACA,MAAA;AACF,MAAA;AACZ,MAAA;AACA,MAAA;AACgB,MAAA;AACzB,MAAA;AACA,MAAA;AACS,MAAA;AACa,MAAA;AACE,MAAA;AACL,MAAA;AACA,MAAA;AACE,MAAA;AACC,MAAA;AAEE,MAAA;AACF,MAAA;AACD,QAAA;AACK,QAAA;AACH,QAAA;AACC,QAAA;AAC9B,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGsB;AACyD,mBAAA;AAAA;AAGG,EAAA;AACjD,IAAA;AACD,IAAA;AACG,IAAA;AACd,IAAA;AACa,IAAA;AACE,IAAA;AACH,MAAA;AACC,MAAA;AACkB,MAAA;AACpC,MAAA;AACoB,MAAA;AACpB,MAAA;AACoB,MAAA;AACE,MAAA;AACM,MAAA;AACL,MAAA;AACH,MAAA;AACF,MAAA;AACF,QAAA;AACxB,QAAA;AACF,MAAA;AAC8B,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACJ,QAAA;AAChB,MAAA;AACuB,MAAA;AACL,MAAA;AACd,MAAA;AAClB,IAAA;AACiC,IAAA;AACD,IAAA;AAClC,EAAA;AACF;AAG6D;AACvB,EAAA;AACF,EAAA;AACX,EAAA;AACa,EAAA;AAC/B,EAAA;AAC+B,IAAA;AACF,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AA2B+B;AAae,EAAA;AAAvB,IAAA;AACJ,IAAA;AACmB,IAAA;AACP,IAAA;AACb,IAAA;AACgB,IAAA;AACD,IAAA;AACE,MAAA;AACD,MAAA;AACD,MAAA;AAClB,MAAA;AACD,MAAA;AACR,IAAA;AACgC,IAAA;AAEF,IAAA;AAClC,EAAA;AAhBqB,EAAA;AAZV,mBAAA;AACM,EAAA;AACA,EAAA;AACA,EAAA;AAC6C,mBAAA;AACE,mBAAA;AACpB,mBAAA;AACZ,mBAAA;AACG,mBAAA;AACA,mBAAA;AACjB,mBAAA;AAoBD,EAAA;AACA,EAAA;AACyD,mBAAA;AAAA;AAGpD,EAAA;AACW,IAAA;AACjC,EAAA;AAAA;AAG0B,EAAA;AACI,IAAA;AACA,IAAA;AACM,IAAA;AACJ,MAAA;AACX,MAAA;AACF,MAAA;AACjB,IAAA;AAC0B,IAAA;AACC,MAAA;AACR,MAAA;AACF,MAAA;AACjB,IAAA;AACU,IAAA;AACwB,IAAA;AACpC,EAAA;AAEqF,EAAA;AACzD,IAAA;AACC,IAAA;AACpB,IAAA;AAEE,MAAA;AACT,IAAA;AACgC,IAAA;AACC,IAAA;AACA,IAAA;AACX,IAAA;AACf,IAAA;AACT,EAAA;AAEgE,EAAA;AACpC,IAAA;AACG,IAAA;AACrB,IAAA;AAC0B,MAAA;AACjB,MAAA;AACS,MAAA;AAC1B,IAAA;AACO,IAAA;AACT,EAAA;AAE2C,EAAA;AACV,IAAA;AACE,IAAA;AACnC,EAAA;AAEoD,EAAA;AACnB,IAAA;AACG,IAAA;AACpC,EAAA;AAEgD,EAAA;AACjB,IAAA;AACH,IAAA;AACT,MAAA;AACiB,MAAA;AACjB,MAAA;AACe,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AAC9B,MAAA;AACH,IAAA;AACO,IAAA;AACT,EAAA;AAEuB,EAAA;AACQ,IAAA;AACZ,IAAA;AACA,IAAA;AACa,IAAA;AACJ,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AAC9B,IAAA;AACM,IAAA;AACT,EAAA;AAE2E,EAAA;AACnD,IAAA;AACS,MAAA;AACD,MAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AACP,IAAA;AACsB,IAAA;AACxB,EAAA;AAE6D,EAAA;AACvC,IAAA;AACP,IAAA;AACc,IAAA;AACC,IAAA;AACE,IAAA;AACI,IAAA;AACD,IAAA;AACJ,IAAA;AACpB,IAAA;AACsB,MAAA;AACA,QAAA;AACL,UAAA;AACA,UAAA;AACM,UAAA;AAC5B,QAAA;AACJ,IAAA;AAC6B,IAAA;AACK,IAAA;AACA,IAAA;AAEF,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AAEP,IAAA;AAEU,MAAA;AACH,MAAA;AACA,MAAA;AAC9B,MAAA;AACF,IAAA;AAEc,IAAA;AACH,IAAA;AACO,IAAA;AACc,MAAA;AACC,MAAA;AACtB,MAAA;AACX,IAAA;AAC6B,IAAA;AACI,IAAA;AACxB,IAAA;AACwB,IAAA;AACpB,IAAA;AACmB,MAAA;AACE,MAAA;AACD,MAAA;AACjC,IAAA;AAEuB,IAAA;AACW,IAAA;AACL,MAAA;AACV,QAAA;AACH,QAAA;AACQ,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACU,QAAA;AACH,QAAA;AACnB,QAAA;AACmB,QAAA;AACV,QAAA;AACD,UAAA;AAEc,YAAA;AACA,YAAA;AAEH,YAAA;AACvB,YAAA;AACF,UAAA;AACa,UAAA;AACY,YAAA;AACG,YAAA;AAC1B,YAAA;AACF,UAAA;AACc,UAAA;AACW,YAAA;AACG,YAAA;AAC1B,YAAA;AACF,UAAA;AACe,UAAA;AACU,YAAA;AACC,YAAA;AACxB,YAAA;AACF,UAAA;AACF,QAAA;AAC6B,QAAA;AAC/B,MAAA;AACF,IAAA;AACiC,IAAA;AACnC,EAAA;AAAA;AAG8F,EAAA;AAC/D,IAAA;AACd,IAAA;AACI,IAAA;AACW,IAAA;AACI,IAAA;AACD,IAAA;AACF,IAAA;AACG,IAAA;AACD,MAAA;AACV,QAAA;AACP,QAAA;AACK,UAAA;AACf,UAAA;AACF,QAAA;AAE2B,QAAA;AACJ,QAAA;AACO,QAAA;AACpB,QAAA;AACkB,QAAA;AAC9B,MAAA;AACF,IAAA;AAE+B,IAAA;AACG,IAAA;AACrB,IAAA;AACf,EAAA;AAEqF,EAAA;AACtD,IAAA;AACK,IAAA;AACL,MAAA;AACE,QAAA;AACf,QAAA;AACiB,QAAA;AACA,QAAA;AACD,QAAA;AACJ,QAAA;AACE,QAAA;AACG,QAAA;AAClB,QAAA;AACb,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAG+G;AACnG,EAAA;AACsB,EAAA;AACA,IAAA;AACE,IAAA;AAClC,EAAA;AACmC,EAAA;AACrC;AAY0E;AACpD,EAAA;AACL,EAAA;AACkB,EAAA;AACnC;AAG2D;AAC/C,EAAA;AAC0B,EAAA;AACW,EAAA;AACd,IAAA;AACF,IAAA;AACI,IAAA;AACnC,EAAA;AAC+B,EAAA;AACG,EAAA;AACF,EAAA;AACzB,EAAA;AACuB,IAAA;AACE,IAAA;AAChC,EAAA;AACF;AH2YsC;AACA;AIz0ByB;AAC1B,EAAA;AACC,EAAA;AAC7B,EAAA;AACT;AAEgE;AAClC,EAAA;AACR,EAAA;AACQ,EAAA;AACO,EAAA;AAC5B,EAAA;AACT;AAEgE;AACxC,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;AAEgC;AAwB0D,EAAA;AAA3D,IAAA;AAAkC,IAAA;AACzC,IAAA;AACH,IAAA;AACa,IAAA;AACJ,IAAA;AACP,IAAA;AACY,IAAA;AAClB,IAAA;AACY,MAAA;AACX,MAAA;AACK,MAAA;AACE,MAAA;AACF,MAAA;AACM,MAAA;AACM,MAAA;AAChB,MAAA;AACf,IAAA;AACoB,IAAA;AAEK,IAAA;AACC,MAAA;AACH,IAAA;AACwD,MAAA;AACtD,MAAA;AACG,IAAA;AACmD,MAAA;AAC/C,MAAA;AACJ,QAAA;AACH,QAAA;AACD,QAAA;AACT,QAAA;AACC,QAAA;AACK,QAAA;AACN,QAAA;AACE,QAAA;AACQ,QAAA;AACtB,MAAA;AACI,IAAA;AACsB,MAAA;AACC,MAAA;AAC9B,IAAA;AACF,EAAA;AAzC6B,EAAA;AAAkC,EAAA;AAvBtD,EAAA;AAAA;AAEsB,mBAAA;AACd,EAAA;AACA,EAAA;AAC2B,mBAAA;AACF,mBAAA;AACI,mBAAA;AACA,mBAAA;AACO,mBAAA;AACI,mBAAA;AACJ,mBAAA;AAAA;AAE7B,mBAAA;AACP,EAAA;AACA,EAAA;AACmB,mBAAA;AACD,mBAAA;AAClB,mBAAA;AACkB,mBAAA;AAAA;AAE4B,mBAAA;AA6CzB,EAAA;AAC7B,IAAA;AACa,MAAA;AACU,MAAA;AACP,MAAA;AACW,MAAA;AACT,MAAA;AACK,QAAA;AACE,QAAA;AACE,UAAA;AACD,UAAA;AACP,UAAA;AACtB,QAAA;AACkB,QAAA;AACS,QAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AAEoB,EAAA;AACL,IAAA;AACf,EAAA;AAAA;AAGwC,EAAA;AACH,IAAA;AACV,IAAA;AACO,IAAA;AACH,IAAA;AACA,IAAA;AACT,IAAA;AACY,MAAA;AAC9B,MAAA;AACF,IAAA;AACgC,IAAA;AAClC,EAAA;AAAA;AAG6B,EAAA;AACK,IAAA;AAClC,EAAA;AAAA;AAGsC,EAAA;AACF,IAAA;AACpC,EAAA;AAAA;AAGqC,EAAA;AACH,IAAA;AACJ,IAAA;AACG,IAAA;AACjC,EAAA;AAAA;AAG0B,EAAA;AACZ,IAAA;AACd,EAAA;AAAA;AAGuB,EAAA;AACY,IAAA;AACZ,IAAA;AACN,IAAA;AACjB,EAAA;AAE0B,EAAA;AACT,IAAA;AACc,IAAA;AACC,IAAA;AACZ,IAAA;AACO,IAAA;AACQ,IAAA;AACnC,EAAA;AAE0C,EAAA;AACrB,IAAA;AACS,IAAA;AACP,IAAA;AACN,IAAA;AACiB,MAAA;AACb,MAAA;AACc,MAAA;AAC/B,MAAA;AACF,IAAA;AACsB,IAAA;AACN,IAAA;AACO,IAAA;AACS,MAAA;AACE,QAAA;AACH,QAAA;AACE,QAAA;AAC/B,MAAA;AACF,IAAA;AAC8B,IAAA;AACI,MAAA;AACF,MAAA;AACV,MAAA;AACW,MAAA;AACJ,MAAA;AAC7B,IAAA;AAC+B,IAAA;AACjC,EAAA;AAAA;AAGyB,EAAA;AACU,IAAA;AACb,IAAA;AACY,MAAA;AAChB,MAAA;AACgB,MAAA;AACzB,MAAA;AACkB,QAAA;AACS,QAAA;AAChC,MAAA;AACY,MAAA;AACd,IAAA;AACgC,IAAA;AAClB,IAAA;AACO,IAAA;AACW,IAAA;AAClB,IAAA;AACkB,IAAA;AACD,MAAA;AACA,MAAA;AACA,MAAA;AACR,MAAA;AACM,MAAA;AAC7B,IAAA;AAE6B,IAAA;AACC,IAAA;AACG,IAAA;AACH,IAAA;AACI,IAAA;AACT,IAAA;AACO,IAAA;AACpB,IAAA;AACd,EAAA;AAAA;AAGe,EAAA;AACI,IAAA;AACH,IAAA;AACmB,IAAA;AACnC,EAAA;AACF;AAGsD;AACpB,EAAA;AACG,EAAA;AAC5B,EAAA;AACT;AAOsD;AACtB,EAAA;AACE,EAAA;AACG,EAAA;AACL,EAAA;AACvB,EAAA;AACT;AJqzBsC;AACA;AKxnCgC;AAGN;AA+BP;AACuB;AAGhB;AAC1B,EAAA;AACD,EAAA;AAChB,EAAA;AACjB,IAAA;AACA,IAAA;AACkC,IAAA;AACF,IAAA;AACF,IAAA;AACI,IAAA;AACH,IAAA;AACjC,EAAA;AACiC,EAAA;AACA,IAAA;AACJ,IAAA;AACK,IAAA;AACE,IAAA;AACT,EAAA;AACI,IAAA;AACG,IAAA;AACG,IAAA;AACT,IAAA;AACM,IAAA;AAC3B,EAAA;AAC6B,IAAA;AACP,IAAA;AACK,MAAA;AACF,MAAA;AACjB,MAAA;AACN,IAAA;AACuB,MAAA;AACD,MAAA;AACA,MAAA;AAC7B,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGqD;AACvB,EAAA;AACN,EAAA;AACa,IAAA;AACI,IAAA;AACL,IAAA;AACA,IAAA;AACGA,MAAAA;AACF,MAAA;AAChC,IAAA;AACH,EAAA;AACmC,EAAA;AACvB,EAAA;AACsB,EAAA;AACxB,EAAA;AACwB,EAAA;AACF,IAAA;AACG,IAAA;AAClC,EAAA;AACH;AAGoG;AACrE,EAAA;AACA,EAAA;AACX,IAAA;AACI,IAAA;AACD,IAAA;AACA,IAAA;AACe,IAAA;AACH,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AAGsF;AACxD,EAAA;AACd,IAAA;AACW,IAAA;AACA,IAAA;AACK,IAAA;AACK,IAAA;AACC,IAAA;AACA,IAAA;AACpC,EAAA;AACsB,EAAA;AACR,IAAA;AACsB,IAAA;AACV,IAAA;AACd,IAAA;AACsB,MAAA;AACD,MAAA;AAC/B,IAAA;AACyB,IAAA;AAC3B,EAAA;AACyB,EAAA;AAC3B;AAM4B;AAGiC,EAAA;AAA9B,IAAA;AAA+B,EAAA;AAA/B,EAAA;AAFrB,EAAA;AAAA;AAKwB,EAAA;AACT,IAAA;AACvB,EAAA;AAEgC,EAAA;AACA,IAAA;AACiB,MAAA;AACd,MAAA;AACA,QAAA;AACR,QAAA;AACO,QAAA;AAC9B,MAAA;AACwB,MAAA;AAC1B,IAAA;AACwB,IAAA;AACQ,IAAA;AACF,IAAA;AAChC,EAAA;AACF;AAGgE;AAC1D,EAAA;AACwB,EAAA;AAEG,IAAA;AACA,IAAA;AACA,IAAA;AACM,IAAA;AACR,EAAA;AACM,IAAA;AACJ,EAAA;AACjB,IAAA;AACkB,IAAA;AACE,EAAA;AACpB,IAAA;AACS,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AAED,IAAA;AACE,IAAA;AACxB,EAAA;AACS,IAAA;AACD,IAAA;AACoB,IAAA;AAChB,MAAA;AACA,MAAA;AACkB,MAAA;AACnC,IAAA;AACgC,IAAA;AACd,IAAA;AACpB,EAAA;AACuB,EAAA;AACzB;AAMyE;AACxC,EAAA;AACL,EAAA;AACU,EAAA;AACH,EAAA;AACC,IAAA;AACpB,MAAA;AACa,MAAA;AACK,QAAA;AACI,QAAA;AACD,QAAA;AACf,QAAA;AAChB,MAAA;AAC2B,MAAA;AAC7B,IAAA;AACF,EAAA;AAC6B,EAAA;AAC/B;AAG6D;AACd,EAAA;AACtC,EAAA;AACF,IAAA;AACgC,IAAA;AACL,IAAA;AACK,IAAA;AACF,IAAA;AACC,IAAA;AACnB,IAAA;AACM,IAAA;AACvB,EAAA;AACF;ALkkCsC;AACA;AMxzCiB;AAuBgC;AACjD,EAAA;AACtC;AAG4D;AACnD,EAAA;AACmB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACA,IAAA;AACjC,EAAA;AACF;AAEgD;AACV,EAAA;AACH,EAAA;AAChB,EAAA;AACC,EAAA;AACA,EAAA;AACD,EAAA;AACgB,EAAA;AACnC;AAG8D;AAC3B,EAAA;AACH,EAAA;AAChC;AAGkD;AACH,EAAA;AACT,EAAA;AACtC;AAEyD;AAEP;AACX,EAAA;AAC3B,EAAA;AACwB,EAAA;AACG,EAAA;AACH,EAAA;AAC3B,EAAA;AACT;AAEiD;AACd,EAAA;AACnC;AAEgE;AAC7B,EAAA;AACnC;AAEkD;AACX,EAAA;AAC3B,EAAA;AACkB,EAAA;AACG,EAAA;AACG,EAAA;AACN,EAAA;AAC9B;AAG0E;AACnC,EAAA;AAC3B,EAAA;AACsB,EAAA;AACJ,EAAA;AACD,EAAA;AACC,EAAA;AACpB,IAAA;AACK,IAAA;AACmB,IAAA;AACJ,IAAA;AAC1B,IAAA;AACgC,IAAA;AAClC,EAAA;AAC+B,EAAA;AACC,EAAA;AACJ,EAAA;AACP,EAAA;AACU,EAAA;AACN,IAAA;AACoB,IAAA;AACrC,MAAA;AACuBC,QAAAA;AACnB,MAAA;AAER,MAAA;AACF,IAAA;AAC8B,IAAA;AAChC,EAAA;AACgC,EAAA;AACJ,EAAA;AACI,EAAA;AACzB,EAAA;AACT;AAGmH;AACxG,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACe,EAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACvB,EAAA;AACmC,EAAA;AACrC;AAmB8F;AACvE,EAAA;AACa,EAAA;AACR,EAAA;AACA,EAAA;AACA,EAAA;AAEc,EAAA;AACD,EAAA;AACE,EAAA;AACd,EAAA;AAC7B;AAGkG;AAC5D,EAAA;AACN,EAAA;AACI,EAAA;AACN,IAAA;AACM,IAAA;AACzB,IAAA;AACR,EAAA;AACuB,EAAA;AACwB,EAAA;AACf,EAAA;AACN,EAAA;AACC,IAAA;AACM,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AN8vCsC;AACA;AOj6CT;AACE;AAEoE;AACrE,EAAA;AACQ,EAAA;AACtC;AAEuB;AAGoF;AAC7E,EAAA;AACK,EAAA;AACH,EAAA;AACH,IAAA;AACF,IAAA;AACzB,EAAA;AACO,EAAA;AACT;AAEuB;AACD;AAGuC;AACzB,EAAA;AACR,EAAA;AAClB,EAAA;AACyB,IAAA;AACL,IAAA;AACL,IAAA;AACvB,EAAA;AACO,EAAA;AACT;AAE2H;AACrF,EAAA;AACX,EAAA;AACjB,EAAA;AACoB,IAAA;AACA,IAAA;AACN,IAAA;AACtB,EAAA;AACO,EAAA;AACT;AAE6D;AACrC,EAAA;AACQ,IAAA;AACA,IAAA;AACR,MAAA;AACF,MAAA;AACU,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACvB,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACgB,EAAA;AACc,IAAA;AACA,IAAA;AACrB,IAAA;AACT,EAAA;AACF;AAGoF;AACrB,EAAA;AAC/D;AAMuE;AACtC,EAAA;AACA,EAAA;AACE,EAAA;AACC,EAAA;AACf,EAAA;AACC,EAAA;AACS,EAAA;AACZ,IAAA;AACC,IAAA;AACa,IAAA;AACZ,MAAA;AACC,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACsB,MAAA;AACE,QAAA;AACA,QAAA;AACD,UAAA;AACH,UAAA;AACC,UAAA;AACV,UAAA;AACI,UAAA;AACA,UAAA;AACd,UAAA;AACA,UAAA;AACP,QAAA;AACF,MAAA;AACyB,MAAA;AACd,MAAA;AACI,QAAA;AACI,QAAA;AACA,QAAA;AACnB,MAAA;AAC6B,MAAA;AAC/B,IAAA;AACF,EAAA;AACO,EAAA;AACT;APm5CsC;AACA;AQ1hDV;AACQ;AA8Bb;AAGmE;AAClD,EAAA;AACF,EAAA;AAC7B,EAAA;AACT;AAgB0H;AACxD,EAAA;AACnC,IAAA;AACnB,IAAA;AACC,IAAA;AACW,IAAA;AACC,IAAA;AACH,IAAA;AACc,IAAA;AACX,IAAA;AACvB,EAAA;AACiC,EAAA;AACK,EAAA;AACF,EAAA;AACE,EAAA;AAC0B,EAAA;AACF,EAAA;AACE,EAAA;AACzD,EAAA;AACT;AAGuC;AACV,EAAA;AACS,EAAA;AACtC;AAGsB;AACa,EAAA;AACG,EAAA;AACtC;AAiB+C;AAClB,EAAA;AACH,EAAA;AACE,IAAA;AACM,MAAA;AACE,MAAA;AACJ,MAAA;AACC,MAAA;AACE,IAAA;AACA,IAAA;AACjC,EAAA;AACO,EAAA;AACT;AAEyE;AACtC,EAAA;AACF,EAAA;AACT,EAAA;AACL,IAAA;AACY,IAAA;AACI,IAAA;AAChC,EAAA;AACM,EAAA;AACT;AAGyC;AACF,EAAA;AAC3B,EAAA;AACqB,EAAA;AACN,EAAA;AACP,IAAA;AAClB,EAAA;AAC4B,EAAA;AACK,EAAA;AACD,EAAA;AACI,EAAA;AACL,EAAA;AACP,EAAA;AACQ,EAAA;AACE,EAAA;AACiB,EAAA;AACvB,EAAA;AACQ,EAAA;AACN,EAAA;AACF,EAAA;AAC9B;AAGsB;AACiB,EAAA;AAC3B,EAAA;AACkB,EAAA;AACP,EAAA;AACN,EAAA;AACmB,EAAA;AACpC;ARq9CsC;AACA;AS3nDd;AACA;AAE2C;AAGZ;AACnB,EAAA;AACpB,EAAA;AACI,EAAA;AACE,EAAA;AACU,EAAA;AAChC;AAEyE;AACtC,EAAA;AACH,EAAA;AACvB,EAAA;AACL,IAAA;AACA,IAAA;AACkC,IAAA;AACJ,IAAA;AAChC,EAAA;AACF;AAOuF;AAC/C,EAAA;AACvB,EAAA;AACc,EAAA;AACH,EAAA;AACA,EAAA;AAEA,EAAA;AACC,EAAA;AACK,EAAA;AACC,EAAA;AACjB,EAAA;AACL,EAAA;AACK,EAAA;AACN,EAAA;AACyB,EAAA;AACrC;AAGkD;AAChB,EAAA;AAClC;AAGkD;AACZ,EAAA;AACtC;AAGkD;AACb,EAAA;AACU,EAAA;AACT,EAAA;AACtC;AAGgH;AAC/E,EAAA;AACA,EAAA;AACI,EAAA;AACrC;AAM4G;AACtE,EAAA;AACjB,EAAA;AACmB,EAAA;AACH,EAAA;AACb,IAAA;AACW,IAAA;AACD,IAAA;AAChC,EAAA;AACiC,EAAA;AACR,IAAA;AACD,IAAA;AACU,IAAA;AAClC,EAAA;AACO,EAAA;AACT;ATqmDsC;AACA;AUrrDmB;AAC/B,EAAA;AAC1B;AAO0E;AAClC,EAAA;AACX,EAAA;AACS,EAAA;AACX,EAAA;AACvB,IAAA;AACA,IAAA;AACmB,IAAA;AACD,IAAA;AACT,IAAA;AACA,IAAA;AACV,EAAA;AAEmC,EAAA;AACnB,IAAA;AACD,IAAA;AAChB,EAAA;AACmC,EAAA;AACH,EAAA;AAClC;AAOmG;AAChF,EAAA;AACQ,EAAA;AACA,EAAA;AACO,EAAA;AACC,EAAA;AACnC;AV0qDsC;AACA;AWluDqB;AAC/C,EAAA;AAIuB,EAAA;AAChB,EAAA;AACF,EAAA;AACc,EAAA;AACK,IAAA;AACE,IAAA;AACR,IAAA;AACI,IAAA;AACJ,IAAA;AACM,IAAA;AACC,IAAA;AACnC,EAAA;AACF;AAMuH;AAC3G,EAAA;AAIyB,EAAA;AACZ,EAAA;AACF,EAAA;AACa,EAAA;AACR,EAAA;AACA,EAAA;AACS,EAAA;AACpB,kBAAA;AACe,EAAA;AAChC;AAG0D;AAC9C,EAAA;AAKsB,EAAA;AACE,EAAA;AACC,EAAA;AAC1B,EAAA;AACI,EAAA;AACL,EAAA;AACgB,EAAA;AAC1B;AXmtDsC;AACA;AYnvDjB;AAEc;AAEZ;AAEyC;AACxC,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;AAGmE;AAC7B,EAAA;AACjB,EAAA;AACZ,EAAA;AACa,IAAA;AACE,IAAA;AACQ,IAAA;AACA,IAAA;AACK,IAAA;AACnC,EAAA;AACF;AA8B6B;AAkBe,EAAA;AAArB,IAAA;AACA,IAAA;AACa,IAAA;AACL,IAAA;AACT,IAAA;AACa,IAAA;AAEJ,IAAA;AAEV,IAAA;AACgB,IAAA;AACA,MAAA;AACP,MAAA;AACF,MAAA;AACvB,IAAA;AAEiC,IAAA;AACL,IAAA;AACM,MAAA;AACV,MAAA;AACjB,MAAA;AACN,IAAA;AACY,IAAA;AACmB,IAAA;AAET,IAAA;AACD,IAAA;AACzB,EAAA;AA3BqB,EAAA;AAjBV,mBAAA;AACiB,mBAAA;AAAA;AAEA,mBAAA;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACqB,mBAAA;AAClB,mBAAA;AACD,mBAAA;AACgB,mBAAA;AACA,mBAAA;AACH,mBAAA;AACA,mBAAA;AACY,mBAAA;AAC3B,EAAA;AA+BA,EAAA;AACyD,mBAAA;AAEzD,EAAA;AAAA;AAGY,EAAA;AACK,IAAA;AAClC,EAAA;AAAA;AAGoB,EAAA;AACN,IAAA;AACd,EAAA;AAAA;AAGgD,EAAA;AACjB,IAAA;AACX,IAAA;AACpB,EAAA;AAAA;AAGuB,EAAA;AACJ,IAAA;AACC,IAAA;AACpB,EAAA;AAAA;AAGsB,EAAA;AACS,IAAA;AACI,IAAA;AACP,IAAA;AACK,MAAA;AACG,MAAA;AACjB,MAAA;AACiB,MAAA;AACjB,MAAA;AACe,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AAC9B,MAAA;AACH,IAAA;AACoB,IAAA;AACD,MAAA;AACA,MAAA;AACa,MAAA;AACJ,QAAA;AACK,QAAA;AACA,QAAA;AACA,QAAA;AAC9B,MAAA;AACe,MAAA;AAClB,IAAA;AAC4B,IAAA;AACrB,IAAA;AACT,EAAA;AAE2E,EAAA;AACnD,IAAA;AACS,MAAA;AACD,MAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAGwF,EAAA;AAC1D,IAAA;AACH,IAAA;AACM,MAAA;AACA,MAAA;AAC/B,IAAA;AACkC,IAAA;AACA,IAAA;AACxB,IAAA;AACuB,IAAA;AACF,IAAA;AACR,IAAA;AACW,IAAA;AACL,MAAA;AACD,QAAA;AACF,UAAA;AACA,UAAA;AACO,UAAA;AAC7B,QAAA;AACa,QAAA;AACY,QAAA;AACI,QAAA;AACL,QAAA;AAC1B,MAAA;AACF,IAAA;AACkB,IAAA;AACpB,EAAA;AAE6D,EAAA;AAC1B,IAAA;AACf,IAAA;AACS,IAAA;AACD,IAAA;AAEO,IAAA;AACnB,IAAA;AACI,IAAA;AACF,MAAA;AACE,QAAA;AACH,QAAA;AACO,QAAA;AAEW,UAAA;AACC,UAAA;AACnB,UAAA;AACX,QAAA;AAC6B,QAAA;AACD,QAAA;AACD,QAAA;AACF,QAAA;AAChB,QAAA;AACgB,QAAA;AACzB,QAAA;AACF,MAAA;AACc,MAAA;AACc,QAAA;AACD,QAAA;AACzB,QAAA;AACF,MAAA;AACK,MAAA;AACwB,QAAA;AAC3B,QAAA;AACW,MAAA;AAEc,QAAA;AACK,QAAA;AAC9B,QAAA;AACF,MAAA;AACK,MAAA;AACuB,QAAA;AAC1B,QAAA;AACa,MAAA;AACW,QAAA;AACxB,QAAA;AACF,MAAA;AACK,MAAA;AACwB,QAAA;AAC3B,QAAA;AACJ,IAAA;AACU,IAAA;AACZ,EAAA;AACF;AAGsF;AACvE,EAAA;AACe,EAAA;AACJ,EAAA;AAC1B;AA8DkE;AAyB1C;AAGO;AAGH;AA0BxB,EAAA;AAFiB,IAAA;AACA,IAAA;AAEiB,IAAA;AACJ,IAAA;AACC,IAAA;AACA,IAAA;AACjC,EAAA;AAPmB,EAAA;AACA,EAAA;AAxBM,mBAAA;AACC,mBAAA;AAC4B,mBAAA;AAC7C,mBAAA;AAAA;AAEI,mBAAA;AACb,EAAA;AACU,mBAAA;AACD,EAAA;AAAA;AAEwB,mBAAA;AACC,mBAAA;AACM,mBAAA;AACpB,mBAAA;AACuB,mBAAA;AAC1B,mBAAA;AACjB,EAAA;AACA,EAAA;AACmC,mBAAA;AACQ,mBAAA;AACC,mBAAA;AAAA;AAAA;AAenB,EAAA;AACO,IAAA;AACA,IAAA;AACX,IAAA;AAEG,MAAA;AACL,MAAA;AACkB,MAAA;AACd,QAAA;AACG,QAAA;AAClB,QAAA;AACF,MAAA;AACW,MAAA;AACmB,MAAA;AACH,MAAA;AAEd,IAAA;AACM,MAAA;AACD,MAAA;AACnB,IAAA;AACL,EAAA;AAAA;AAGyC,EAAA;AACN,IAAA;AACV,IAAA;AACzB,EAAA;AAAA;AAG2D,EAAA;AAC7B,IAAA;AACK,MAAA;AACJ,MAAA;AAC7B,IAAA;AACkC,IAAA;AACnB,IAAA;AACK,IAAA;AACQ,sBAAA;AACR,sBAAA;AACP,MAAA;AACO,MAAA;AACQ,IAAA;AACL,IAAA;AACzB,EAAA;AAE4B,EAAA;AACX,IAAA;AACW,IAAA;AACE,IAAA;AACM,IAAA;AACL,IAAA;AACA,IAAA;AACI,IAAA;AACH,IAAA;AACL,IAAA;AAC3B,EAAA;AAAA;AAGwB,EAAA;AACS,IAAA;AACD,IAAA;AACb,IAAA;AACW,MAAA;AACE,MAAA;AACE,MAAA;AACL,MAAA;AACN,MAAA;AACrB,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAGiC,EAAA;AAChB,IAAA;AACA,IAAA;AACqB,IAAA;AACF,IAAA;AACpC,EAAA;AAAA;AAGwB,EAAA;AACW,IAAA;AACnC,EAAA;AAAA;AAGoB,EAAA;AACa,IAAA;AACjC,EAAA;AAAA;AAG4B,EAAA;AACM,IAAA;AAClC,EAAA;AAEoD,EAAA;AACjB,IAAA;AACzB,IAAA;AACQ,MAAA;AACc,MAAA;AAC9B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG8D,EAAA;AACjC,IAAA;AACF,IAAA;AACA,MAAA;AACG,MAAA;AACM,MAAA;AACD,MAAA;AACD,MAAA;AAChC,IAAA;AACO,IAAA;AACT,EAAA;AAEkE,EAAA;AAC/B,IAAA;AACH,IAAA;AACI,IAAA;AACb,IAAA;AACvB,EAAA;AAEoE,EAAA;AACjC,IAAA;AACA,IAAA;AACC,IAAA;AACD,MAAA;AACH,MAAA;AAC9B,IAAA;AACmB,IAAA;AACrB,EAAA;AAE2C,EAAA;AACP,IAAA;AACL,oBAAA;AACP,IAAA;AACxB,EAAA;AAAA;AAGqB,EAAA;AACE,oBAAA;AACvB,EAAA;AAAA;AAGoC,EAAA;AACD,IAAA;AACmB,IAAA;AAC7C,IAAA;AACL,MAAA;AACsB,MAAA;AACI,MAAA;AACE,MAAA;AACI,MAAA;AACV,MAAA;AACJ,MAAA;AACpB,IAAA;AACF,EAAA;AAEiC,EAAA;AACC,IAAA;AAClC,EAAA;AAAA;AAGgD,EAAA;AAC/B,IAAA;AACC,IAAA;AACC,IAAA;AACY,IAAA;AACC,IAAA;AACI,MAAA;AACH,MAAA;AACN,MAAA;AACzB,IAAA;AAC6B,IAAA;AACtB,IAAA;AACT,EAAA;AAAA;AAG2C,EAAA;AAC1B,IAAA;AACC,IAAA;AACC,IAAA;AACY,IAAA;AACC,IAAA;AACC,MAAA;AACT,MAAA;AACS,MAAA;AACN,MAAA;AACzB,IAAA;AAC6B,IAAA;AACtB,IAAA;AACT,EAAA;AAAA;AAG0B,EAAA;AACT,IAAA;AACP,IAAA;AACsB,IAAA;AACD,MAAA;AACF,MAAA;AACV,MAAA;AACU,MAAA;AAC3B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO+C,EAAA;AAC3B,IAAA;AAEE,MAAA;AACN,MAAA;AACd,IAAA;AAEiC,IAAA;AACf,IAAA;AACF,IAAA;AACD,IAAA;AACmB,IAAA;AACf,MAAA;AACM,MAAA;AACzB,IAAA;AACmB,IAAA;AACA,MAAA;AACF,MAAA;AACQ,MAAA;AACzB,IAAA;AAEkC,IAAA;AAClB,IAAA;AACa,IAAA;AACI,IAAA;AACH,IAAA;AAChB,IAAA;AACH,IAAA;AACQ,MAAA;AACM,MAAA;AACzB,IAAA;AACkB,IAAA;AACO,IAAA;AACnB,MAAA;AACY,QAAA;AACd,MAAA;AACe,QAAA;AACjB,MAAA;AACiB,MAAA;AACA,MAAA;AACM,QAAA;AACH,QAAA;AACd,QAAA;AACI,UAAA;AACN,QAAA;AACkB,UAAA;AACpB,QAAA;AACuB,MAAA;AACV,MAAA;AACG,MAAA;AACjB,IAAA;AACY,IAAA;AACR,IAAA;AACT,EAAA;AAAA;AAGqC,EAAA;AACpB,IAAA;AACmB,IAAA;AACC,IAAA;AAChB,IAAA;AACL,IAAA;AACO,IAAA;AACH,IAAA;AACS,IAAA;AACQ,MAAA;AAClC,IAAA;AACH,EAAA;AAE4B,EAAA;AACX,IAAA;AACQ,IAAA;AACD,IAAA;AACC,IAAA;AACU,IAAA;AACnC,EAAA;AAAA;AAG2D,mBAAA;AAAA;AAGb,EAAA;AAC5B,IAAA;AACD,IAAA;AACA,IAAA;AAEP,IAAA;AACQ,IAAA;AACA,IAAA;AACiB,IAAA;AACR,IAAA;AACnB,MAAA;AACY,QAAA;AACd,MAAA;AACe,QAAA;AACjB,MAAA;AACC,IAAA;AACY,IAAA;AACT,IAAA;AACsB,IAAA;AACL,IAAA;AACR,IAAA;AACE,IAAA;AACnB,EAAA;AAAA;AAAA;AAAA;AAAA;AAMgG,EAAA;AAC/E,IAAA;AACC,IAAA;AACqB,IAAA;AAC+B,IAAA;AACnC,IAAA;AACf,IAAA;AACH,IAAA;AACR,IAAA;AACT,EAAA;AAAA;AAGmD,EAAA;AAClC,IAAA;AACkB,IAAA;AACJ,IAAA;AAC/B,EAAA;AAAA;AAG+B,EAAA;AACd,IAAA;AACgB,IAAA;AACb,IAAA;AACpB,EAAA;AAAA;AAGwC,EAAA;AAC3B,IAAA;AACkB,MAAA;AAC3B,MAAA;AACF,IAAA;AAC+B,IAAA;AACb,IAAA;AACpB,EAAA;AAAA;AAGc,EAAA;AACQ,oBAAA;AACP,IAAA;AACC,IAAA;AACM,IAAA;AACJ,IAAA;AACC,IAAA;AACnB,EAAA;AAAA;AAG2F,EAAA;AAC1E,IAAA;AACC,IAAA;AACyD,IAAA;AAC5C,IAAA;AACA,IAAA;AACI,IAAA;AACA,IAAA;AACH,IAAA;AACE,MAAA;AACW,QAAA;AACT,QAAA;AACH,UAAA;AACjB,UAAA;AACc,UAAA;AACK,UAAA;AAC7B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACK,IAAA;AACT,EAAA;AAEgB,EAAA;AACM,oBAAA;AACP,IAAA;AACK,oBAAA;AACP,IAAA;AACb,EAAA;AACF;AAGkE;AACjC,EAAA;AACE,EAAA;AACnC;AZ+jDsC;AACA;Aat2EJ;AAGa;AACf,EAAA;AACR,oBAAA;AACa,IAAA;AACR,EAAA;AACjB,EAAA;AACgB,EAAA;AACN,EAAA;AACa,EAAA;AACC,EAAA;AAEC,IAAA;AACF,IAAA;AACG,IAAA;AACpC,EAAA;AACmC,EAAA;AAC1B,IAAA;AACC,IAAA;AACR,IAAA;AACiC,IAAA;AACD,IAAA;AACT,IAAA;AACL,IAAA;AACe,IAAA;AAC3B,IAAA;AACgB,IAAA;AACvB,EAAA;AACkC,EAAA;AACrB,EAAA;AAEmB,EAAA;AACP,EAAA;AAC5B;AAU+C;AACpB,EAAA;AACf,EAAA;AACS,EAAA;AACC,EAAA;AACS,EAAA;AACb,IAAA;AACoB,IAAA;AACF,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AAGgD;AACb,EAAA;AACpB,EAAA;AACqB,EAAA;AACd,EAAA;AAClB,IAAA;AACgB,MAAA;AAAC,MAAA;AACM,MAAA;AACL,QAAA;AACa,QAAA;AAC7B,MAAA;AACF,IAAA;AACA,IAAA;AACW,MAAA;AACT,MAAA;AACqB,MAAA;AACH,MAAA;AAClB,MAAA;AACoB,MAAA;AACY,MAAA;AACT,MAAA;AACL,MAAA;AACZ,MAAA;AACI,MAAA;AACK,MAAA;AACO,MAAA;AACQ,MAAA;AACN,MAAA;AACE,MAAA;AAC5B,IAAA;AACF,EAAA;AACoC,EAAA;AACxB,EAAA;AAEsB,EAAA;AACpC;AAEwD;AAClB,EAAA;AACH,EAAA;AAER,EAAA;AACE,EAAA;AACQ,EAAA;AACO,EAAA;AACf,EAAA;AAEQ,IAAA;AACJ,EAAA;AAEP,EAAA;AAEW,IAAA;AACA,IAAA;AACA,IAAA;AACnC,EAAA;AAC+B,EAAA;AACe,EAAA;AACX,EAAA;AACN,EAAA;AAC/B;Abo1EsC;AACA;Acl9EyB;AAkEuB;AAC7E,EAAA;AAC4B,IAAA;AACF,IAAA;AACtB,IAAA;AACI,IAAA;AACF,IAAA;AACb,EAAA;AACF;AAE8B;AAC2B;AACF;AAEsE;AACjH,EAAA;AACc,EAAA;AACjB,EAAA;AAC2B,IAAA;AACH,IAAA;AACI,IAAA;AACF,IAAA;AACH,IAAA;AAC9B,EAAA;AACF;AAGoD;AAC7B,EAAA;AACA,EAAA;AAEK,EAAA;AACL,EAAA;AACK,EAAA;AACS,EAAA;AACrC;AAGmD;AAC3B,EAAA;AACA,EAAA;AACc,EAAA;AACtC;AAE+C;AAGgB;AAC/B,EAAA;AACC,EAAA;AACJ,EAAA;AACO,EAAA;AACC,EAAA;AACrC;AAO2D;AAC7C,EAAA;AACI,EAAA;AACT,IAAA;AACK,MAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AAC0B,MAAA;AAClB,IAAA;AACI,MAAA;AACa,MAAA;AAC9B,IAAA;AACqB,IAAA;AACI,MAAA;AACX,MAAA;AACoB,MAAA;AAClB,MAAA;AACkB,QAAA;AACF,QAAA;AAC9B,MAAA;AACO,MAAA;AACT,IAAA;AACF,EAAA;AACF;AAGuD;AACf,EAAA;AACF,EAAA;AACA,IAAA;AACJ,IAAA;AACG,IAAA;AACxB,IAAA;AACA,IAAA;AACA,IAAA;AACmB,IAAA;AACE,MAAA;AACG,QAAA;AACxB,MAAA;AACsB,QAAA;AACH,QAAA;AACG,QAAA;AACA,QAAA;AAC7B,MAAA;AACF,IAAA;AAC8B,IAAA;AACG,IAAA;AACnC,EAAA;AACO,EAAA;AACT;AAM+F;AACnF,EAAA;AACQ,EAAA;AACW,EAAA;AACI,IAAA;AACC,IAAA;AACb,MAAA;AACa,MAAA;AACD,QAAA;AAC7B,MAAA;AACwB,MAAA;AACzB,IAAA;AACH,EAAA;AACyB,EAAA;AACM,EAAA;AACC,EAAA;AAClC;AAGgF;AACvD,EAAA;AACG,EAAA;AACL,IAAA;AACA,IAAA;AACU,IAAA;AACC,IAAA;AAChC,EAAA;AAEmC,EAAA;AACA,EAAA;AAC5B,EAAA;AACT;AAGoC;AAOJ;AAIwB,EAAA;AAAzB,IAAA;AAA0B,EAAA;AAA1B,EAAA;AAHE,mBAAA;AACF,mBAAA;AAAA;AAK0B,EAAA;AACnB,IAAA;AACpC,EAAA;AAEgC,EAAA;AACG,IAAA;AAChB,IAAA;AACe,MAAA;AACvB,MAAA;AACT,IAAA;AACe,IAAA;AACA,IAAA;AACM,MAAA;AACA,MAAA;AACU,MAAA;AACG,MAAA;AACN,MAAA;AACZ,MAAA;AACE,MAAA;AACe,QAAA;AACV,QAAA;AACP,UAAA;AACHC,UAAAA;AACT,QAAA;AACF,MAAA;AACW,MAAA;AACO,MAAA;AACpB,IAAA;AACe,IAAA;AACkB,IAAA;AACA,IAAA;AACnC,EAAA;AACF;AAG2D;AACnB,EAAA;AACH,EAAA;AACT,EAAA;AAEM,IAAA;AACC,IAAA;AACG,IAAA;AACL,IAAA;AACK,IAAA;AACF,IAAA;AACD,MAAA;AACE,MAAA;AACjC,IAAA;AACF,EAAA;AACO,EAAA;AACT;Ad82EsC;AACA;Ae5oF1B;AAEyC;AACxB,EAAA;AACI,EAAA;AACJ,EAAA;AACS,EAAA;AACtC;AAEuE;AACnC,EAAA;AACpC;AAG+F;AAC/E,EAAA;AACA,EAAA;AACA,EAAA;AACc,EAAA;AACA,EAAA;AACZ,EAAA;AACR,EAAA;AACG,EAAA;AACsB,IAAA;AACA,IAAA;AACF,IAAA;AAC/B,EAAA;AACgB,EAAA;AACmB,EAAA;AACrC;AAEkE;AACzB,EAAA;AACzB,EAAA;AACoB,EAAA;AACpB,EAAA;AACqB,EAAA;AACA,EAAA;AACrC;AAG+F;AAC/D,EAAA;AACE,EAAA;AACA,EAAA;AACR,EAAA;AACR,EAAA;AACkB,EAAA;AACpC;AAEkE;AAC/B,EAAA;AACf,EAAA;AACe,EAAA;AACnC;AAMmD;AAC5C,EAAA;AACE,EAAA;AACC,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACX;AAEmD;AACnB,EAAA;AACF,EAAA;AACE,EAAA;AACH,IAAA;AACJ,IAAA;AACc,IAAA;AACrC,EAAA;AACO,EAAA;AACT;AAGqE;AAChC,EAAA;AACrC;AAGmE;AAChC,EAAA;AACnC;AAIuE;AAaT;AAC7B,EAAA;AACC,EAAA;AACJ,EAAA;AACI,EAAA;AACY,EAAA;AACR,IAAA;AACJ,IAAA;AACI,IAAA;AACpC,EAAA;AACiC,EAAA;AACwC,EAAA;AACjD,IAAA;AACW,IAAA;AACH,IAAA;AACT,IAAA;AACK,IAAA;AACF,IAAA;AAC1B,EAAA;AAC8B,EAAA;AACA,IAAA;AACG,IAAA;AACP,IAAA;AACU,MAAA;AACF,MAAA;AAC/B,IAAA;AACH,EAAA;AACoC,EAAA;AACtC;AAWwD;AACjB,EAAA;AAC3B,EAAA;AACuB,EAAA;AACA,EAAA;AACd,EAAA;AACS,EAAA;AACQ,EAAA;AACJ,EAAA;AACzB,EAAA;AACT;AAGmD;AACvB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACU,EAAA;AACV,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAC5B;AfimFsC;AACA;AgB/vFN;AACC;AAEiB;AAC5C,EAAA;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACyB,EAAA;AACC,EAAA;AACnC;AAEiD;AAC5C,EAAA;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACI,EAAA;AACoB,EAAA;AAA4G;AAE3G,EAAA;AAClC;AAGgD;AAC3C,EAAA;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACsB,EAAA;AAC/B;AAoBsB;AACJ,mBAAA;AACC,mBAAA;AAC4B,mBAAA;AAAA;AAEpC,mBAAA;AAAA;AAGsK,EAAA;AAChJ,IAAA;AACF,IAAA;AACI,MAAA;AACD,QAAA;AAC7B,MAAA;AACF,IAAA;AAC6B,IAAA;AACG,IAAA;AACD,IAAA;AACjC,EAAA;AAAA;AAGuJ,EAAA;AACpH,IAAA;AACC,IAAA;AACd,IAAA;AACF,MAAA;AAAiC,MAAA;AACjD,MAAA;AACW,QAAA;AACK,QAAA;AACO,QAAA;AACH,QAAA;AAClB,QAAA;AACoB,QAAA;AACQ,QAAA;AACrB,QAAA;AACK,QAAA;AACN,QAAA;AACI,QAAA;AACK,QAAA;AACO,QAAA;AACQ,QAAA;AAChC,MAAA;AACF,IAAA;AACmB,IAAA;AACK,IAAA;AACZ,IAAA;AACK,MAAA;AACV,MAAA;AACP,IAAA;AAC4B,IAAA;AAC9B,EAAA;AAAA;AAGsD,EAAA;AACtB,IAAA;AACG,IAAA;AACC,IAAA;AACA,IAAA;AACF,IAAA;AACC,IAAA;AACN,IAAA;AACnB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACuB,IAAA;AACX,MAAA;AACL,MAAA;AACI,MAAA;AACA,MAAA;AACZ,MAAA;AACP,IAAA;AACoB,IAAA;AACsB,IAAA;AACR,IAAA;AACpC,EAAA;AAEc,EAAA;AACiB,IAAA;AACxB,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY2J,EAAA;AAC5H,IAAA;AACG,IAAA;AACP,IAAA;AACM,MAAA;AAC9B,IAAA;AACiC,IAAA;AACN,IAAA;AACM,IAAA;AACD,IAAA;AACC,IAAA;AACL,IAAA;AACA,IAAA;AACE,IAAA;AACb,MAAA;AAClB,IAAA;AAC+B,IAAA;AACG,IAAA;AAEA,IAAA;AACJ,IAAA;AACP,IAAA;AACU,MAAA;AACD,MAAA;AACE,MAAA;AACD,MAAA;AACA,MAAA;AAClB,MAAA;AACgB,MAAA;AAC9B,IAAA;AAEiC,IAAA;AACD,IAAA;AACL,MAAA;AACZ,MAAA;AACgB,MAAA;AACG,QAAA;AACD,QAAA;AACA,QAAA;AAChC,MAAA;AAC+B,MAAA;AACjC,IAAA;AACc,IAAA;AACkB,MAAA;AACL,MAAA;AAC3B,IAAA;AACiC,IAAA;AACJ,IAAA;AACG,IAAA;AACE,IAAA;AACpC,EAAA;AAAA;AAGgB,EAAA;AACgB,IAAA;AAChC,EAAA;AAAA;AAG2B,EAAA;AACI,IAAA;AACA,IAAA;AACF,IAAA;AACK,IAAA;AACC,IAAA;AAC5B,IAAA;AACP,EAAA;AAAA;AAGqB,EAAA;AACO,IAAA;AAC5B,EAAA;AAAA;AAG4B,EAAA;AACC,IAAA;AACI,IAAA;AACP,MAAA;AACS,MAAA;AACA,MAAA;AACA,MAAA;AACnB,MAAA;AACd,IAAA;AACO,IAAA;AACT,EAAA;AACF;AhBguFsC;AACA;AiBv4FuB;AACC,EAAA;AAChD,EAAA;AACuB,EAAA;AACX,EAAA;AACA,EAAA;AACY,EAAA;AACD,EAAA;AACrC;AAGoE;AAC9C,EAAA;AACgB,EAAA;AAC7B,EAAA;AACT;AAQE;AAG2B,EAAA;AACa,EAAA;AACR,EAAA;AACL,EAAA;AACO,EAAA;AACpB,EAAA;AAC6B,EAAA;AACP,IAAA;AACf,IAAA;AACF,IAAA;AACe,MAAA;AACJ,MAAA;AAClB,MAAA;AACoB,MAAA;AAC9B,IAAA;AACF,EAAA;AAC6F,EAAA;AACnE,EAAA;AACU,IAAA;AACP,IAAA;AACV,MAAA;AACkB,MAAA;AACH,MAAA;AACD,MAAA;AACG,MAAA;AACsB,QAAA;AAC3B,QAAA;AACG,MAAA;AAC9B,MAAA;AACF,IAAA;AACyB,IAAA;AACR,MAAA;AACM,MAAA;AACE,MAAA;AACnB,MAAA;AACc,QAAA;AACF,UAAA;AACK,YAAA;AACQ,YAAA;AACN,YAAA;AACM,YAAA;AACvB,YAAA;AACF,UAAA;AACK,UAAA;AACoB,YAAA;AACvB,YAAA;AACG,UAAA;AACqB,YAAA;AACxB,YAAA;AACW,UAAA;AAC4B,YAAA;AACd,YAAA;AACF,YAAA;AACC,YAAA;AACxB,YAAA;AACF,UAAA;AACkB,UAAA;AACJ,YAAA;AACY,YAAA;AACxB,YAAA;AACF,UAAA;AACF,QAAA;AACM,MAAA;AAER,MAAA;AACA,MAAA;AACF,IAAA;AAC0B,IAAA;AAC1B,IAAA;AACkC,IAAA;AACP,IAAA;AACM,IAAA;AAEV,MAAA;AACM,MAAA;AACJ,MAAA;AACO,MAAA;AACD,QAAA;AACN,QAAA;AACkB,QAAA;AACzC,MAAA;AACqB,MAAA;AACT,MAAA;AACe,MAAA;AACJ,QAAA;AACE,QAAA;AACO,QAAA;AAChC,MAAA;AACK,IAAA;AACgB,MAAA;AACS,MAAA;AAChC,IAAA;AACgC,IAAA;AAClC,EAAA;AACK,EAAA;AACP;AAG4E;AACvC,EAAA;AACH,EAAA;AAClC;AAGmD;AAClB,EAAA;AACA,EAAA;AACR,EAAA;AACO,EAAA;AACF,EAAA;AACI,EAAA;AACI,IAAA;AACD,IAAA;AACJ,IAAA;AACE,IAAA;AACG,IAAA;AACpC,EAAA;AACgD,EAAA;AAChB,EAAA;AACR,EAAA;AAC1B;AAGiE;AAC7B,EAAA;AACL,EAAA;AACA,EAAA;AACJ,EAAA;AACQ,EAAA;AACH,EAAA;AAClB,EAAA;AACC,EAAA;AACqB,EAAA;AACF,IAAA;AACpB,IAAA;AACC,IAAA;AACsB,IAAA;AACnC,EAAA;AACO,EAAA;AACsB,EAAA;AACF,IAAA;AACR,IAAA;AACiB,MAAA;AACH,sBAAA;AAC/B,IAAA;AACiC,IAAA;AAClC,EAAA;AAEiC,EAAA;AACL,EAAA;AAC/B;AAG4D;AACzC,EAAA;AACiB,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACd,EAAA;AACK,EAAA;AACQ,IAAA;AACJ,IAAA;AACM,IAAA;AACA,IAAA;AAClC,EAAA;AACiC,EAAA;AACpC;AASuE;AACtC,EAAA;AACV,EAAA;AACa,EAAA;AAA+B;AAA2F;AAAO;AACxI,EAAA;AAC7B;AjB82FsC;AACA;AkBpjGA;AACP,EAAA;AACd,IAAA;AACD,IAAA;AACd,EAAA;AACF;AA0MY;AAE+B;AACPC,EAAAA;AACF,EAAA;AAClC;AAE+D;AAC9B,EAAA;AACX,EAAA;AACtB;AAE2D;AACvB,EAAA;AACpC;AAEkF;AAClD,EAAA;AACR,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;AAEsF;AAEzE;AACH,EAAA;AACuC,mBAAA;AACV,mBAAA;AACA,mBAAA;AACpB,EAAA;AACT,EAAA;AACA,EAAA;AAAA;AAEwC,mBAAA;AACxC,EAAA;AACiB,mBAAA;AAAA;AAEsB,mBAAA;AAChB,mBAAA;AACwB,mBAAA;AAC/C,EAAA;AACS,EAAA;AACuB,mBAAA;AACtB,mBAAA;AACwB,mBAAA;AACG,mBAAA;AACqB,mBAAA;AACO,mBAAA;AACnC,mBAAA;AAChB,mBAAA;AACE,mBAAA;AAChB,EAAA;AAC2C,mBAAA;AACd,mBAAA;AACE,mBAAA;AACnB,mBAAA;AACK,mBAAA;AACuC,mBAAA;AAC7B,mBAAA;AACT,mBAAA;AACQ,mBAAA;AACE,mBAAA;AAAsB;AAEjD,EAAA;AACQ,EAAA;AAAA;AAEuC,mBAAA;AACD,mBAAA;AAAA;AAEkB,mBAAA;AAAA;AAElB,mBAAA;AAET,EAAA;AACZ,IAAA;AACC,IAAA;AACL,IAAA;AACC,IAAA;AACI,IAAA;AACJ,IAAA;AACA,IAAA;AACF,IAAA;AACM,IAAA;AACP,IAAA;AACX,IAAA;AACb,MAAA;AACyB,QAAA;AACD,QAAA;AACA,QAAA;AACK,UAAA;AACE,UAAA;AAC7B,QAAA;AAC2B,QAAA;AACH,UAAA;AACM,UAAA;AAEF,UAAA;AACD,UAAA;AAC3B,QAAA;AACe,QAAA;AAAC,QAAA;AAClB,MAAA;AACQ,MAAA;AACV,IAAA;AAC8B,IAAA;AAChC,EAAA;AAAA;AAI4C,EAAA;AACf,IAAA;AACC,IAAA;AAC9B,EAAA;AAEwD,EAAA;AAC1B,IAAA;AACC,IAAA;AACxB,IAAA;AACS,IAAA;AACoB,IAAA;AACpC,EAAA;AAAA;AAG6B,EAAA;AACE,IAAA;AACI,IAAA;AACA,IAAA;AACnB,IAAA;AACG,MAAA;AACH,MAAA;AACmB,MAAA;AACnB,MAAA;AACS,MAAA;AACW,MAAA;AAChB,MAAA;AACL,MAAA;AACA,MAAA;AACa,MAAA;AACA,MAAA;AACE,MAAA;AACA,MAAA;AACb,MAAA;AACE,MAAA;AACY,MAAA;AACK,MAAA;AACF,MAAA;AACzB,MAAA;AACgB,QAAA;AACC,QAAA;AACH,QAAA;AACC,QAAA;AACG,QAAA;AACL,QAAA;AACO,QAAA;AACE,QAAA;AACX,QAAA;AACc,QAAA;AAC9B,MAAA;AACc,MAAA;AAChB,IAAA;AACY,IAAA;AACd,EAAA;AAEmC,EAAA;AACrB,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkC,EAAA;AACjB,IAAA;AACD,IAAA;AACP,IAAA;AACT,EAAA;AAAA;AAIwC,EAAA;AACV,IAAA;AAC9B,EAAA;AAEsE,EAAA;AACvB,IAAA;AAC7B,IAAA;AACa,MAAA;AACd,MAAA;AACf,IAAA;AACyB,IAAA;AACQ,IAAA;AACf,IAAA;AACP,IAAA;AACS,IAAA;AACV,IAAA;AACZ,EAAA;AAAA;AAG+B,EAAA;AACZ,IAAA;AACL,IAAA;AAEe,IAAA;AACJ,IAAA;AACK,MAAA;AACA,MAAA;AACX,QAAA;AACC,QAAA;AAChB,MAAA;AACiC,MAAA;AACnC,IAAA;AACF,EAAA;AAAA;AAG2C,EAAA;AACZ,IAAA;AAC/B,EAAA;AAAA;AAG2B,EAAA;AACL,IAAA;AACY,IAAA;AAClC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMyD,EAAA;AACrB,IAAA;AACN,IAAA;AACZ,IAAA;AACkB,IAAA;AACP,IAAA;AACpB,IAAA;AACT,EAAA;AAAA;AAGwC,oBAAA;AAES,EAAA;AAClB,IAAA;AAC/B,EAAA;AAAA;AAG+F,EAAA;AAC1D,IAAA;AACrC,EAAA;AAAA;AAGmE,EAAA;AAC9B,IAAA;AACX,IAAA;AACC,IAAA;AACG,IAAA;AAC9B,EAAA;AAEsB,EAAA;AACM,IAAA;AACI,IAAA;AACf,IAAA;AACgB,IAAA;AACZ,IAAA;AACE,IAAA;AACE,IAAA;AACV,IAAA;AACF,IAAA;AACqB,MAAA;AACA,MAAA;AACA,MAAA;AAChC,IAAA;AACU,IAAA;AACD,IAAA;AACX,EAAA;AAEsB,EAAA;AACM,IAAA;AACI,IAAA;AACf,IAAA;AACgB,IAAA;AACZ,IAAA;AACE,IAAA;AACE,IAAA;AACV,IAAA;AACF,IAAA;AACqB,MAAA;AACA,MAAA;AAChC,IAAA;AACU,IAAA;AACD,IAAA;AACX,EAAA;AAAA;AAGoD,EAAA;AAChB,IAAA;AACpC,EAAA;AAAA;AAIiG,EAAA;AAChE,IAAA;AACF,MAAA;AACT,MAAA;AACX,MAAA;AACT,IAAA;AACiB,IAAA;AACnB,EAAA;AAAA;AAGgH,EAAA;AAC7F,IAAA;AACgB,IAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACJ,MAAA;AAC1B,IAAA;AACkB,IAAA;AACa,IAAA;AAC3B,IAAA;AACe,IAAA;AACF,MAAA;AACD,MAAA;AACjB,IAAA;AAC8B,IAAA;AACvB,IAAA;AACT,EAAA;AAAA;AAGqD,EAAA;AAClC,IAAA;AACc,IAAA;AACG,MAAA;AACF,MAAA;AACF,MAAA;AAC7B,IAAA;AAC6B,IAAA;AAEC,MAAA;AACd,MAAA;AACjB,IAAA;AACgC,IAAA;AACzB,IAAA;AACT,EAAA;AAAA;AAG8E,EAAA;AAC3D,IAAA;AACG,IAAA;AACe,IAAA;AACJ,MAAA;AACX,MAAA;AACX,MAAA;AACT,IAAA;AACgC,IAAA;AACF,IAAA;AACI,IAAA;AACF,IAAA;AACC,IAAA;AACR,IAAA;AACM,IAAA;AACT,IAAA;AACC,IAAA;AACzB,EAAA;AAAA;AAI6C,EAAA;AACT,IAAA;AACpC,EAAA;AAAA;AAGyC,EAAA;AACP,IAAA;AAClC,EAAA;AAAA;AAG+B,EAAA;AACE,IAAA;AACqC,IAAA;AACpC,MAAA;AACZ,QAAA;AACY,QAAA;AACC,QAAA;AAC/B,MAAA;AACF,IAAA;AAC6B,IAAA;AACtB,IAAA;AACT,EAAA;AAEuD,EAAA;AACrB,IAAA;AACR,IAAA;AACI,MAAA;AACM,MAAA;AACX,IAAA;AAChB,IAAA;AACC,MAAA;AACE,MAAA;AACA,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACiB,MAAA;AACE,MAAA;AACF,MAAA;AAC1B,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAE6C,EAAA;AACjB,IAAA;AACR,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAGgF,EAAA;AAC/C,IAAA;AACZ,IAAA;AACW,IAAA;AACE,IAAA;AACA,IAAA;AACN,IAAA;AACnB,IAAA;AACT,EAAA;AAEiC,EAAA;AACX,IAAA;AACe,IAAA;AAET,IAAA;AAChB,IAAA;AACZ,EAAA;AAAA;AAGgI,EAAA;AAC7F,IAAA;AACA,IAAA;AACJ,IAAA;AACG,IAAA;AAEX,IAAA;AACS,IAAA;AAC1B,IAAA;AAC8B,IAAA;AACT,IAAA;AACK,MAAA;AACE,MAAA;AACjB,MAAA;AACG,IAAA;AACe,MAAA;AACX,MAAA;AACS,MAAA;AACT,MAAA;AACtB,IAAA;AACgC,IAAA;AACD,IAAA;AACA,IAAA;AACjC,EAAA;AAE6D,EAAA;AAC1C,IAAA;AACa,IAAA;AACC,IAAA;AACK,IAAA;AACD,IAAA;AACN,MAAA;AAC7B,IAAA;AACkC,IAAA;AACd,IAAA;AACgB,IAAA;AACF,IAAA;AACA,IAAA;AAC0B,IAAA;AAC1B,IAAA;AACF,IAAA;AACE,MAAA;AACA,MAAA;AAClC,IAAA;AACkC,IAAA;AACJ,IAAA;AAChC,EAAA;AAE2E,EAAA;AACxD,IAAA;AACa,IAAA;AACD,IAAA;AACL,IAAA;AACL,IAAA;AACD,MAAA;AACa,MAAA;AAC/B,IAAA;AACiC,IAAA;AACX,IAAA;AACW,IAAA;AACA,IAAA;AACX,IAAA;AACxB,EAAA;AAE2C,EAAA;AACxB,IAAA;AACgB,IAAA;AACgB,MAAA;AACpB,MAAA;AAC7B,IAAA;AAC+B,IAAA;AACX,IAAA;AACwC,IAAA;AACD,IAAA;AACf,IAAA;AACf,MAAA;AACE,MAAA;AACE,MAAA;AACD,MAAA;AAChC,IAAA;AACkC,IAAA;AACF,IAAA;AACD,IAAA;AACF,IAAA;AAC/B,EAAA;AAAA;AAG+E,EAAA;AAC5C,IAAA;AACF,IAAA;AACX,IAAA;AACQ,IAAA;AAChB,IAAA;AACkB,MAAA;AACE,MAAA;AACE,MAAA;AAClC,IAAA;AACkC,IAAA;AACP,IAAA;AACK,IAAA;AACL,IAAA;AACI,IAAA;AACD,IAAA;AAChC,EAAA;AAAA;AAGsC,EAAA;AACV,IAAA;AACK,IAAA;AACtB,IAAA;AACsB,MAAA;AACA,MAAA;AAC/B,IAAA;AAC+B,IAAA;AACX,IAAA;AACc,IAAA;AACP,IAAA;AACT,IAAA;AACW,IAAA;AACI,IAAA;AACL,IAAA;AACA,IAAA;AAClB,IAAA;AACkC,MAAA;AAEV,MAAA;AACrB,MAAA;AACmB,MAAA;AAChC,IAAA;AAC4C,IAAA;AACX,IAAA;AACF,IAAA;AACC,IAAA;AAClC,EAAA;AAAA;AAG6E,EAAA;AAChD,IAAA;AACA,IAAA;AACA,IAAA;AACT,MAAA;AACe,MAAA;AACjC,IAAA;AACkC,IAAA;AACF,IAAA;AACD,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMmF,EAAA;AACvD,IAAA;AACF,IAAA;AACA,IAAA;AACK,IAAA;AACF,IAAA;AACK,IAAA;AACR,IAAA;AACS,IAAA;AACL,IAAA;AACF,IAAA;AACD,IAAA;AACM,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AACD,IAAA;AACA,IAAA;AACP,IAAA;AACD,IAAA;AACM,MAAA;AACG,MAAA;AACH,MAAA;AACC,MAAA;AAChC,IAAA;AAC2B,IAAA;AACnB,MAAA;AACa,MAAA;AACS,MAAA;AAC5B,MAAA;AACA,MAAA;AACgC,MAAA;AACrB,MAAA;AACD,MAAA;AACZ,IAAA;AACqB,IAAA;AACU,IAAA;AACC,IAAA;AAClC,EAAA;AAEmF,EAAA;AAClE,IAAA;AACiB,IAAA;AACP,IAAA;AACG,MAAA;AACI,MAAA;AACN,MAAA;AACK,MAAA;AACR,MAAA;AACvB,IAAA;AACkC,IAAA;AACpC,EAAA;AAAA;AAIsD,EAAA;AACvB,IAAA;AAC/B,EAAA;AAAA;AAG6C,EAAA;AAChB,IAAA;AACO,IAAA;AACpC,EAAA;AAE6C,EAAA;AAChB,IAAA;AACT,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiF,EAAA;AACpD,IAAA;AACU,IAAA;AAEF,IAAA;AACF,IAAA;AAEA,IAAA;AAChB,IAAA;AACJ,IAAA;AACD,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAGuF,EAAA;AACrD,IAAA;AACE,IAAA;AACH,IAAA;AACG,IAAA;AACrB,IAAA;AACc,IAAA;AACO,IAAA;AACF,IAAA;AACjB,IAAA;AACH,IAAA;AACL,IAAA;AACT,EAAA;AAE8B,EAAA;AACM,IAAA;AACP,IAAA;AACN,IAAA;AACO,IAAA;AACb,IAAA;AACH,IAAA;AACd,EAAA;AAAA;AAG+B,EAAA;AACA,IAAA;AAC/B,EAAA;AAAA;AAGyD,EAAA;AAC3B,IAAA;AACX,IAAA;AACnB,EAAA;AAAA;AAAA;AAAA;AAAA;AAM8H,EAAA;AAChG,IAAA;AACxB,IAAA;AACA,IAAA;AAC6B,MAAA;AACrB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACyB,IAAA;AACE,IAAA;AACL,IAAA;AACY,MAAA;AACb,QAAA;AACK,QAAA;AACtB,QAAA;AACF,MAAA;AAC2B,MAAA;AACK,MAAA;AACb,QAAA;AACjB,QAAA;AACF,MAAA;AAC8B,MAAA;AACF,MAAA;AACb,MAAA;AACjB,IAAA;AACkB,IAAA;AACD,MAAA;AACH,MAAA;AACd,IAAA;AACkC,IAAA;AACpC,EAAA;AAAA;AAG8G,EAAA;AAC3E,IAAA;AACA,IAAA;AACnC,EAAA;AAAA;AAAA;AAKgC,EAAA;AACD,IAAA;AACb,IAAA;AACJ,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQmD,EAAA;AAC7C,IAAA;AACA,IAAA;AACwB,MAAA;AAChB,IAAA;AACO,MAAA;AACX,MAAA;AACR,IAAA;AACwB,IAAA;AACD,MAAA;AACd,MAAA;AACT,IAAA;AACgB,IAAA;AACJ,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO4G,EAAA;AAC7E,IAAA;AACO,IAAA;AACN,IAAA;AACG,MAAA;AACC,MAAA;AACF,IAAA;AACA,IAAA;AAClC,EAAA;AAEmD,EAAA;AAC7C,IAAA;AACyB,MAAA;AACjB,IAAA;AACO,MAAA;AACX,MAAA;AACR,IAAA;AACF,EAAA;AAEyC,EAAA;AACN,IAAA;AACrB,IAAA;AACA,IAAA;AACd,EAAA;AAEmB,EAAA;AACe,IAAA;AACpB,IAAA;AACd,EAAA;AAAA;AAGsG,EAAA;AACrE,IAAA;AACR,IAAA;AACA,IAAA;AACU,IAAA;AACE,IAAA;AACL,MAAA;AACI,MAAA;AACD,MAAA;AACC,MAAA;AAClC,IAAA;AACgC,IAAA;AACE,IAAA;AACpC,EAAA;AAAA;AAAA;AAK0C,EAAA;AACZ,IAAA;AACI,IAAA;AACpB,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAGiC,EAAA;AACG,IAAA;AACtB,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAAA;AAK+C,EAAA;AACX,IAAA;AACtB,IAAA;AACA,IAAA;AACd,EAAA;AAEuB,EAAA;AACI,IAAA;AAC3B,EAAA;AAE0B,EAAA;AACQ,IAAA;AACpB,IAAA;AACA,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAIuD,EAAA;AACxB,IAAA;AACL,IAAA;AACjB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAM2D,EAAA;AACzB,IAAA;AACD,IAAA;AACjC,EAAA;AAAA;AAGmC,EAAA;AACC,IAAA;AACpC,EAAA;AAEgH,EAAA;AAChF,IAAA;AACG,MAAA;AACA,MAAA;AACC,MAAA;AAClC,IAAA;AACmB,IAAA;AACc,MAAA;AACZ,MAAA;AACO,MAAA;AACN,MAAA;AACN,MAAA;AACkB,MAAA;AACA,MAAA;AACN,MAAA;AACnB,MAAA;AACN,IAAA;AAC4B,IAAA;AACE,IAAA;AACD,IAAA;AACT,MAAA;AACE,IAAA;AACI,MAAA;AAC7B,IAAA;AACI,IAAA;AACmB,IAAA;AACS,MAAA;AACF,MAAA;AAEiB,QAAA;AACjB,QAAA;AACI,QAAA;AACtB,QAAA;AACV,MAAA;AAC4B,MAAA;AACC,QAAA;AAC7B,MAAA;AACgB,MAAA;AACO,MAAA;AACN,MAAA;AACe,MAAA;AACJ,MAAA;AACH,MAAA;AACC,MAAA;AACC,MAAA;AACK,MAAA;AACb,MAAA;AACjB,QAAA;AACQ,QAAA;AACR,QAAA;AAC8B,QAAA;AAC9B,QAAA;AAC2B,QAAA;AACL,QAAA;AACI,QAAA;AACpB,QAAA;AACuB,QAAA;AACH,QAAA;AACnB,QAAA;AACR,MAAA;AAC8B,MAAA;AACA,MAAA;AACC,IAAA;AAEF,MAAA;AACD,MAAA;AACrB,MAAA;AACV,IAAA;AAC2B,IAAA;AACC,IAAA;AACS,IAAA;AACL,IAAA;AACX,IAAA;AACI,IAAA;AACF,IAAA;AACA,IAAA;AACQ,IAAA;AACN,IAAA;AACQ,IAAA;AACF,IAAA;AACX,IAAA;AACH,MAAA;AACA,MAAA;AACD,MAAA;AACkB,MAAA;AAChC,MAAA;AACO,MAAA;AACsB,MAAA;AACF,MAAA;AACL,MAAA;AACtB,MAAA;AACU,MAAA;AACI,MAAA;AACe,MAAA;AAC7B,MAAA;AACA,MAAA;AACwB,MAAA;AACD,MAAA;AACvB,MAAA;AACwB,MAAA;AACE,MAAA;AACC,MAAA;AAC5B,IAAA;AAC8B,IAAA;AACA,IAAA;AACxB,IAAA;AACT,EAAA;AAE4D,EAAA;AACrC,IAAA;AACU,IAAA;AACD,MAAA;AACX,MAAA;AACY,MAAA;AACjB,MAAA;AACgB,MAAA;AACjB,MAAA;AACb,IAAA;AAC8B,IAAA;AACC,IAAA;AACC,IAAA;AACC,IAAA;AACb,IAAA;AACK,MAAA;AACE,MAAA;AAC3B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAM4D,EAAA;AACzC,IAAA;AACG,IAAA;AACY,IAAA;AACH,MAAA;AACG,QAAA;AAEJ,UAAA;AACtB,UAAA;AACF,QAAA;AACuB,QAAA;AACE,QAAA;AACD,QAAA;AACT,QAAA;AACjB,MAAA;AACqB,MAAA;AACJ,QAAA;AACH,QAAA;AACM,QAAA;AACD,QAAA;AACW,QAAA;AACJ,QAAA;AACH,QAAA;AACO,QAAA;AACD,QAAA;AACT,UAAA;AAEU,YAAA;AACX,YAAA;AACY,YAAA;AACD,YAAA;AACE,YAAA;AACJ,YAAA;AACxB,UAAA;AACU,UAAA;AACV,UAAA;AACF,QAAA;AAC4B,QAAA;AACA,QAAA;AACF,QAAA;AACF,UAAA;AACK,UAAA;AACA,UAAA;AACF,UAAA;AAC3B,QAAA;AAC0B,QAAA;AACC,QAAA;AACG,QAAA;AACH,QAAA;AACE,QAAA;AACJ,QAAA;AAC3B,MAAA;AACF,IAAA;AACI,IAAA;AACA,IAAA;AAC6B,MAAA;AACrB,IAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AACe,IAAA;AACH,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG8C,EAAA;AAChC,IAAA;AACd,EAAA;AAAA;AAG0K,EAAA;AAC9J,IAAA;AACsB,IAAA;AAChB,MAAA;AAAC,MAAA;AACM,MAAA;AACQ,QAAA;AACC,QAAA;AACA,QAAA;AACJ,QAAA;AACM,QAAA;AACH,QAAA;AACA,QAAA;AACrB,QAAA;AACD,UAAA;AACkB,UAAA;AACK,UAAA;AACE,UAAA;AACA,UAAA;AAC9B,QAAA;AACF,MAAA;AACF,IAAA;AACgC,IAAA;AACb,IAAA;AACQ,IAAA;AACL,IAAA;AACF,MAAA;AACS,MAAA;AACG,QAAA;AACD,QAAA;AACG,QAAA;AACN,QAAA;AAC1B,MAAA;AAC4B,MAAA;AAC9B,IAAA;AACkC,IAAA;AACpC,EAAA;AAAA;AAGwF,EAAA;AAC3D,IAAA;AACK,IAAA;AACZ,IAAA;AAChB,IAAA;AACA,IAAA;AAC6B,MAAA;AACrB,IAAA;AACG,MAAA;AACP,MAAA;AACR,IAAA;AAC+B,IAAA;AACJ,IAAA;AACO,IAAA;AACjB,IAAA;AACV,IAAA;AACT,EAAA;AAAA;AAGqD,EAAA;AACpB,IAAA;AACN,IAAA;AACA,IAAA;AACO,MAAA;AACI,MAAA;AACxB,MAAA;AACX,IAAA;AACM,IAAA;AACT,EAAA;AAAA;AAGwC,EAAA;AACtB,IAAA;AAClB,EAAA;AAAA;AAAA;AAAA;AAAA;AAM8G,EAAA;AAChF,IAAA;AACI,IAAA;AACE,IAAA;AACA,MAAA;AACd,MAAA;AACX,MAAA;AACR,IAAA;AACgC,IAAA;AACA,IAAA;AACI,IAAA;AACJ,IAAA;AACE,IAAA;AACA,IAAA;AACd,IAAA;AACR,IAAA;AACoB,MAAA;AACD,MAAA;AAChC,IAAA;AAC6B,IAAA;AACd,IAAA;AACiB,IAAA;AACT,IAAA;AACN,IAAA;AACD,IAAA;AACiB,IAAA;AACC,MAAA;AACP,MAAA;AACI,QAAA;AACb,QAAA;AACU,QAAA;AACM,UAAA;AACA,UAAA;AAC9B,QAAA;AACwB,QAAA;AACG,QAAA;AAEd,QAAA;AACiB,QAAA;AACA,UAAA;AACV,UAAA;AACpB,QAAA;AAEyB,QAAA;AACE,QAAA;AACjB,QAAA;AACiB,QAAA;AACE,QAAA;AACT,QAAA;AACtB,MAAA;AACF,IAAA;AACkC,IAAA;AACD,IAAA;AACnC,EAAA;AAAA;AAAA;AAKwF,EAAA;AACnE,IAAA;AACc,IAAA;AACd,IAAA;AACc,IAAA;AACb,IAAA;AACtB,EAAA;AAAA;AAG6B,EAAA;AACD,IAAA;AAC5B,EAAA;AAAA;AAG2D,EAAA;AAChC,IAAA;AACY,IAAA;AACH,IAAA;AACnB,IAAA;AACW,IAAA;AAC5B,EAAA;AAAA;AAGqG,EAAA;AACpE,IAAA;AACf,IAAA;AACI,IAAA;AACU,IAAA;AACd,IAAA;AACO,IAAA;AACS,IAAA;AAClC,EAAA;AAAA;AAGyE,EAAA;AAC1C,IAAA;AACF,IAAA;AACO,IAAA;AACA,IAAA;AACJ,IAAA;AACN,IAAA;AAC1B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASqD,EAAA;AAClB,IAAA;AACF,IAAA;AACG,IAAA;AACJ,IAAA;AACI,IAAA;AACf,IAAA;AACS,IAAA;AACI,IAAA;AACJ,IAAA;AACrB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOoE,EAAA;AAC9C,IAAA;AAChB,IAAA;AAC0B,MAAA;AAC5B,IAAA;AACe,MAAA;AACjB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQyF,EAAA;AAChE,IAAA;AACS,IAAA;AAClC,EAAA;AAEsF,EAAA;AAChE,IAAA;AAChB,IAAA;AAC6B,MAAA;AACV,MAAA;AACW,MAAA;AAChC,IAAA;AACe,MAAA;AACjB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQoC,EAAA;AACV,IAAA;AAC1B,EAAA;AAAA;AAG2C,EAAA;AACR,IAAA;AACnC,EAAA;AAAA;AAGgB,EAAA;AACG,IAAA;AACnB,EAAA;AAAA;AAGyB,EAAA;AACF,IAAA;AACvB,EAAA;AAE6E,EAAA;AAC5C,IAAA;AACZ,IAAA;AACW,IAAA;AACE,IAAA;AACzB,IAAA;AACT,EAAA;AAEwE,EAAA;AACpD,IAAA;AACa,IAAA;AACG,IAAA;AACF,IAAA;AACL,IAAA;AACpB,IAAA;AACT,EAAA;AAAA;AAG0I,EAAA;AAC7G,IAAA;AACM,IAAA;AACA,IAAA;AACnC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO2B,EAAA;AACE,IAAA;AAC7B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ6F,EAAA;AAC3D,IAAA;AACE,IAAA;AACT,IAAA;AACI,IAAA;AACnB,IAAA;AACkB,IAAA;AAClB,IAAA;AACH,IAAA;AACT,EAAA;AAAA;AAG6C,EAAA;AACjB,IAAA;AAC5B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOsJ,EAAA;AACrH,IAAA;AACE,IAAA;AACN,IAAA;AACjB,IAAA;AACe,IAAA;AACM,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOqJ,EAAA;AAC1H,IAAA;AACG,IAAA;AACO,IAAA;AACP,IAAA;AACI,IAAA;AACE,IAAA;AACd,IAAA;AACJ,IAAA;AACkB,IAAA;AACP,IAAA;AACG,IAAA;AACC,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsI,EAAA;AACnH,IAAA;AACe,IAAA;AACA,IAAA;AACC,IAAA;AACC,IAAA;AACN,IAAA;AACA,IAAA;AACR,IAAA;AACJ,IAAA;AACkB,IAAA;AACD,IAAA;AACX,IAAA;AACW,IAAA;AACH,IAAA;AACA,IAAA;AAChC,EAAA;AAAA;AAG2D,EAAA;AAC/B,IAAA;AAC5B,EAAA;AAAA;AAGiF,EAAA;AAC9C,IAAA;AACb,IAAA;AACc,IAAA;AACJ,IAAA;AAClB,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGmF,EAAA;AAChD,IAAA;AACb,IAAA;AACY,IAAA;AACF,IAAA;AAClB,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGiD,EAAA;AAC3B,IAAA;AACgB,IAAA;AACN,IAAA;AACA,IAAA;AAClB,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGuC,EAAA;AACnB,IAAA;AACN,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGmC,EAAA;AACG,IAAA;AACnB,IAAA;AACL,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGoC,EAAA;AACZ,IAAA;AACV,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG2C,EAAA;AACtB,IAAA;AACP,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGgG,EAAA;AACnE,IAAA;AACM,IAAA;AACnC,EAAA;AAAA;AAIkB,EAAA;AACgB,IAAA;AAClC,EAAA;AAAA;AAGsC,EAAA;AAClB,IAAA;AACS,IAAA;AACO,IAAA;AACD,IAAA;AACH,IAAA;AACvB,IAAA;AACT,EAAA;AAAA;AAGwC,EAAA;AAChB,IAAA;AACQ,IAAA;AACH,IAAA;AAC7B,EAAA;AAAA;AAGgD,EAAA;AACf,IAAA;AACG,IAAA;AACH,IAAA;AACE,IAAA;AACC,IAAA;AACW,IAAA;AACb,IAAA;AACzB,IAAA;AACT,EAAA;AAAA;AAGgD,EAAA;AACb,IAAA;AACT,IAAA;AACO,IAAA;AACrB,IAAA;AACY,IAAA;AACG,MAAA;AACU,MAAA;AACnC,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGsD,EAAA;AAC7B,IAAA;AACL,IAAA;AACU,IAAA;AAClB,IAAA;AACM,IAAA;AACM,IAAA;AACS,MAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AACA,MAAA;AACP,MAAA;AACK,MAAA;AAC/B,IAAA;AACO,IAAA;AACT,EAAA;AAEwE,EAAA;AAC3B,IAAA;AACrB,IAAA;AACU,MAAA;AACL,MAAA;AACK,MAAA;AAChC,IAAA;AAC6C,IAAA;AAChB,IAAA;AACF,IAAA;AAC7B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOsH,EAAA;AACpF,IAAA;AACZ,IAAA;AACtB,EAAA;AAAA;AAGyD,EAAA;AACxB,IAAA;AACb,IAAA;AACW,IAAA;AAC/B,EAAA;AAAA;AAG8E,EAAA;AAC1C,IAAA;AACJ,IAAA;AACI,IAAA;AACf,IAAA;AACc,IAAA;AACA,IAAA;AACH,IAAA;AAChC,EAAA;AAAA;AAAA;AAAA;AAMyC,EAAA;AACP,IAAA;AACL,IAAA;AAC7B,EAAA;AAAA;AAG2B,EAAA;AACyB,IAAA;AACjB,IAAA;AACN,IAAA;AACF,IAAA;AACS,IAAA;AACpC,EAAA;AAEwD,EAAA;AACvC,IAAA;AACY,IAAA;AACjB,IAAA;AACwB,IAAA;AACT,IAAA;AACM,MAAA;AACJ,QAAA;AACP,UAAA;AACd,UAAA;AACF,QAAA;AACU,QAAA;AACkB,QAAA;AAC9B,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGmD,EAAA;AACS,IAAA;AACf,IAAA;AACb,IAAA;AACI,MAAA;AACJ,MAAA;AACF,MAAA;AAC5B,IAAA;AACkC,IAAA;AAClB,IAAA;AAC6B,IAAA;AACb,IAAA;AACE,IAAA;AAClB,IAAA;AACc,IAAA;AACd,IAAA;AACT,IAAA;AACT,EAAA;AAAA;AAGmH,EAAA;AAC9E,IAAA;AACd,IAAA;AACU,IAAA;AACgB,MAAA;AACb,MAAA;AACD,QAAA;AACgB,QAAA;AACd,QAAA;AACD,QAAA;AAChC,MAAA;AAC0B,MAAA;AAC5B,IAAA;AACgB,IAAA;AACW,IAAA;AACmB,IAAA;AACjB,IAAA;AACI,MAAA;AAC3B,MAAA;AACyB,QAAA;AACjB,MAAA;AACI,QAAA;AAChB,MAAA;AACF,IAAA;AAC0B,IAAA;AAC5B,EAAA;AAEqE,EAAA;AACrC,IAAA;AAChC,EAAA;AAAA;AAG8G,EAAA;AACrG,IAAA;AACT,EAAA;AAEkM,EAAA;AACvK,IAAA;AACO,IAAA;AACN,IAAA;AACK,IAAA;AACE,IAAA;AACJ,IAAA;AACC,MAAA;AACC,MAAA;AAC/B,IAAA;AACkB,IAAA;AACA,IAAA;AACe,IAAA;AACV,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AAChB,IAAA;AACI,IAAA;AAChB,EAAA;AAAA;AAGiC,EAAA;AACd,IAAA;AACb,IAAA;AACgB,MAAA;AACR,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAG8E,EAAA;AAC3D,IAAA;AACb,IAAA;AACsB,MAAA;AACd,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAGwE,EAAA;AACvC,IAAA;AACC,IAAA;AAClC,EAAA;AAEwE,EAAA;AACpC,IAAA;AACP,IAAA;AACO,IAAA;AACd,IAAA;AACa,IAAA;AACA,IAAA;AACH,IAAA;AAChC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiI,EAAA;AAC9F,IAAA;AACL,IAAA;AACG,IAAA;AACP,IAAA;AACS,IAAA;AACJ,IAAA;AACT,IAAA;AACG,IAAA;AACW,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO6B,EAAA;AACO,IAAA;AACH,IAAA;AACN,IAAA;AACW,IAAA;AAC5B,MAAA;AACF,MAAA;AACE,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACY,MAAA;AACQ,MAAA;AACN,MAAA;AACvB,IAAA;AACoB,IAAA;AAEJ,IAAA;AAEdC,IAAAA;AAC4B,MAAA;AACK,MAAA;AACN,MAAA;AACM,MAAA;AAChC,IAAA;AAC4B,IAAA;AACG,IAAA;AACN,IAAA;AACM,MAAA;AACF,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AAC/B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGsH,EAAA;AACrF,IAAA;AACF,IAAA;AAC/B,EAAA;AAAA;AAIkD,EAAA;AAClC,IAAA;AACC,IAAA;AACR,IAAA;AACT,EAAA;AAAA;AAG2D,EAAA;AACxC,IAAA;AACgB,IAAA;AAC1B,IAAA;AACT,EAAA;AAAA;AAGoE,EAAA;AAC9D,IAAA;AACA,IAAA;AACY,MAAA;AACJ,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AAC1B,IAAA;AACT,EAAA;AAAA;AAGqH,EAAA;AAC/G,IAAA;AACc,IAAA;AACgB,MAAA;AACJ,MAAA;AACI,QAAA;AAChC,MAAA;AACgC,MAAA;AACF,IAAA;AACE,MAAA;AACA,IAAA;AACf,IAAA;AACW,IAAA;AACE,IAAA;AACX,IAAA;AACvB,EAAA;AAE6D,EAAA;AAC3B,IAAA;AACb,IAAA;AACY,IAAA;AACR,IAAA;AACnB,IAAA;AAC2B,MAAA;AACE,MAAA;AACH,MAAA;AACrB,MAAA;AACG,IAAA;AACoB,MAAA;AAChC,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMqJ,EAAA;AACtH,IAAA;AACE,IAAA;AACG,IAAA;AACD,IAAA;AACW,IAAA;AACb,IAAA;AACjC,EAAA;AAAA;AAGgH,EAAA;AACzF,IAAA;AACe,IAAA;AACH,IAAA;AACH,IAAA;AACzB,MAAA;AACgC,MAAA;AACA,MAAA;AACP,MAAA;AACG,MAAA;AACjC,IAAA;AACgC,IAAA;AACE,IAAA;AACD,IAAA;AAC1B,IAAA;AACT,EAAA;AAE0B,EAAA;AACnB,IAAA;AAC4B,IAAA;AACnC,EAAA;AAAA;AAGwD,EAAA;AAChC,IAAA;AACP,IAAA;AACmB,IAAA;AACA,IAAA;AACA,IAAA;AACH,IAAA;AACC,IAAA;AACE,IAAA;AACD,IAAA;AACnC,EAAA;AAAA;AAGgJ,EAAA;AACjH,IAAA;AACD,IAAA;AACM,MAAA;AACF,MAAA;AAC9B,MAAA;AACc,MAAA;AACC,MAAA;AACW,MAAA;AACA,MAAA;AACJ,MAAA;AACb,MAAA;AACX,IAAA;AACiC,IAAA;AAC1B,IAAA;AACT,EAAA;AAEqD,EAAA;AAClB,IAAA;AACf,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAGyI,EAAA;AACrG,IAAA;AACN,IAAA;AACmB,IAAA;AACd,IAAA;AAC1B,IAAA;AACT,EAAA;AAEuC,EAAA;AACb,IAAA;AACS,IAAA;AACnC,EAAA;AAAA;AAGwC,EAAA;AACT,IAAA;AAC/B,EAAA;AAAA;AAG6D,EAAA;AACzB,IAAA;AACD,IAAA;AACX,IAAA;AACA,IAAA;AACW,IAAA;AACF,IAAA;AACA,IAAA;AACjC,EAAA;AAEsF,EAAA;AACnD,IAAA;AACnB,IAAA;AACT,IAAA;AACsB,IAAA;AACjB,IAAA;AAC4B,IAAA;AACpB,IAAA;AACd,IAAA;AACmB,MAAA;AACX,IAAA;AACI,MAAA;AAChB,IAAA;AACY,IAAA;AACc,IAAA;AACpB,MAAA;AAC4B,QAAA;AACb,QAAA;AACR,UAAA;AACG,UAAA;AACC,UAAA;AACY,UAAA;AACL,UAAA;AACK,UAAA;AACP,UAAA;AAClB,QAAA;AACU,MAAA;AACwB,QAAA;AACpC,MAAA;AACF,IAAA;AACiC,IAAA;AACqB,MAAA;AAC3B,QAAA;AACnB,QAAA;AAC0B,UAAA;AACH,UAAA;AACR,UAAA;AACH,UAAA;AACJ,QAAA;AACwB,UAAA;AACpC,QAAA;AACF,MAAA;AACF,IAAA;AAC+B,IAAA;AACV,MAAA;AAC2B,MAAA;AACxC,QAAA;AACqB,UAAA;AACF,UAAA;AACO,UAAA;AACC,UAAA;AACnB,QAAA;AACe,UAAA;AAC3B,QAAA;AACD,MAAA;AACH,IAAA;AACc,IAAA;AAChB,EAAA;AAAA;AAAA;AAK0F,EAAA;AACrE,IAAA;AACc,IAAA;AACd,IAAA;AACc,IAAA;AACV,IAAA;AACzB,EAAA;AAAA;AAGsE,EAAA;AAC5D,IAAA;AACyB,IAAA;AACtB,IAAA;AACb,EAAA;AAEmG,EAAA;AAChE,IAAA;AACP,IAAA;AACD,IAAA;AACG,IAAA;AAC9B,EAAA;AAAA;AAG0C,EAAA;AACT,IAAA;AACd,IAAA;AACG,IAAA;AACU,IAAA;AAChC,EAAA;AAAA;AAG8D,EAAA;AAC3B,IAAA;AACF,IAAA;AACJ,IAAA;AACH,IAAA;AAC1B,EAAA;AAAA;AAGsG,EAAA;AACnE,IAAA;AACH,IAAA;AACT,IAAA;AACjB,IAAA;AACA,IAAA;AACyB,MAAA;AACjB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACkC,IAAA;AACd,IAAA;AACH,IAAA;AACnB,EAAA;AAAA;AAGqG,EAAA;AAClE,IAAA;AACN,IAAA;AACK,IAAA;AACF,IAAA;AACI,IAAA;AACd,IAAA;AACO,IAAA;AAC7B,EAAA;AAAA;AAG+E,EAAA;AAC5C,IAAA;AAC7B,IAAA;AAAgC,MAAA;AAA6I,IAAA;AAChJ,IAAA;AACb,IAAA;AACb,IAAA;AACT,EAAA;AAAA;AAGqE,EAAA;AAClC,IAAA;AACD,IAAA;AAClC,EAAA;AAAA;AAIgI,oBAAA;AACxG,oBAAA;AAAA;AAGqC,EAAA;AAC5B,IAAA;AACA,IAAA;AACA,IAAA;AACN,IAAA;AACE,IAAA;AACpB,IAAA;AACT,EAAA;AAAA;AAG0B,EAAA;AACS,IAAA;AACnC,EAAA;AAAA;AAGyC,EAAA;AACnC,IAAA;AACA,IAAA;AACyB,MAAA;AACjB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AACb,IAAA;AACtB,EAAA;AAE4C,EAAA;AACX,IAAA;AACb,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAGkF,EAAA;AAC/C,IAAA;AAC7B,IAAA;AACA,IAAA;AAC2B,MAAA;AACnB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AAChB,IAAA;AACnB,EAAA;AAAA;AAGkD,EAAA;AACjB,IAAA;AACE,IAAA;AACA,IAAA;AAC1B,IAAA;AACT,EAAA;AAAA;AAAA;AAKkH,EAAA;AAC5G,IAAA;AAC0B,MAAA;AAClB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAGgE,EAAA;AACrC,IAAA;AACb,IAAA;AACA,IAAA;AACd,EAAA;AAEuE,EAAA;AAC1C,IAAA;AACE,IAAA;AACK,IAAA;AACH,IAAA;AACxB,IAAA;AACT,EAAA;AAEsC,EAAA;AACJ,IAAA;AACF,IAAA;AAClB,IAAA;AACA,IAAA;AACd,EAAA;AAEmD,EAAA;AACnB,IAAA;AAClB,IAAA;AACA,IAAA;AACd,EAAA;AAAA;AAI2B,EAAA;AACb,IAAA;AACd,EAAA;AAAA;AAGiH,EAAA;AAC/E,IAAA;AACI,IAAA;AAChC,IAAA;AACwB,MAAA;AACd,MAAA;AACL,MAAA;AACG,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAG0G,EAAA;AACpG,IAAA;AACA,IAAA;AAC8B,MAAA;AACtB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACgC,IAAA;AACzB,IAAA;AACT,EAAA;AAEuB,EAAA;AACN,IAAA;AACH,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO2J,EAAA;AACvH,IAAA;AAC9B,IAAA;AACA,IAAA;AAC8B,MAAA;AACtB,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACiC,IAAA;AACrB,IAAA;AACe,IAAA;AACM,IAAA;AACnC,EAAA;AAAA;AAGuB,EAAA;AACC,IAAA;AACxB,EAAA;AAAA;AAGmC,EAAA;AAC7B,IAAA;AACkB,MAAA;AACV,IAAA;AAC8B,MAAA;AAC1C,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAGuC,EAAA;AACJ,IAAA;AACnC,EAAA;AAAA;AAGiD,EAAA;AACjB,IAAA;AACC,IAAA;AACc,IAAA;AACtB,IAAA;AACX,IAAA;AACmB,IAAA;AACjC,EAAA;AAE+C,EAAA;AACd,IAAA;AACb,IAAA;AACX,IAAA;AACT,EAAA;AAAA;AAG8C,EAAA;AAChB,IAAA;AACF,IAAA;AACM,IAAA;AAClC,EAAA;AAAA;AAGgD,EAAA;AAClB,IAAA;AACI,IAAA;AACpB,IAAA;AACoB,IAAA;AAClC,EAAA;AAEmF,EAAA;AACrD,IAAA;AACO,IAAA;AACrC,EAAA;AAE8D,EAAA;AAC1C,IAAA;AACD,IAAA;AACF,IAAA;AACkB,IAAA;AACvB,IAAA;AACe,IAAA;AACnB,MAAA;AACwB,QAAA;AAChB,MAAA;AACI,QAAA;AAChB,MAAA;AACF,IAAA;AAC4B,IAAA;AACA,MAAA;AACd,QAAA;AACiB,QAAA;AACX,UAAA;AACd,UAAA;AACF,QAAA;AAC2B,QAAA;AACC,QAAA;AACE,QAAA;AACJ,QAAA;AAC3B,MAAA;AACH,IAAA;AACF,EAAA;AAAA;AAG8C,EAAA;AACnB,IAAA;AACE,IAAA;AAC7B,EAAA;AACF;AAK8B;AAGwE;AAClE,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACL,EAAA;AACC,EAAA;AACf,EAAA;AACgB,EAAA;AACI,IAAA;AACT,IAAA;AACU,IAAA;AAClB,IAAA;AACe,IAAA;AACG,MAAA;AACC,MAAA;AACA,MAAA;AACR,MAAA;AACQ,MAAA;AAChB,MAAA;AACY,MAAA;AACA,MAAA;AACI,QAAA;AACA,QAAA;AACH,QAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AACkC,EAAA;AACpC;AlB6oFsC;AACA;AmBnzKH;AAAe;AAwBpC;AAGwD;AACtC,EAAA;AACR,EAAA;AACa,EAAA;AACJ,EAAA;AACK,IAAA;AACpC,EAAA;AACc,EAAA;AACqB,EAAA;AACH,EAAA;AACE,EAAA;AACpC;AAGkE;AAC1C,EAAA;AACa,EAAA;AAC/B,EAAA;AAC2B,EAAA;AAC1B,EAAA;AACU,IAAA;AACc,IAAA;AACb,IAAA;AAChB,EAAA;AACmC,EAAA;AACrC;AAGuF;AAClD,EAAA;AACb,EAAA;AACc,EAAA;AACU,EAAA;AACf,EAAA;AACK,IAAA;AACH,IAAA;AACjC,EAAA;AACO,EAAA;AACT;AAEgD;AAUrB;AAON,EAAA;AAFA,IAAA;AACR,IAAA;AACQ,IAAA;AAEa,IAAA;AACI,IAAA;AACpC,EAAA;AANmB,EAAA;AACR,EAAA;AACQ,EAAA;AANF,EAAA;AACA,EAAA;AAAA;AAY4F,EAAA;AAC9E,IAAA;AACgB,MAAA;AACX,MAAA;AAC1B,QAAA;AACuB,UAAA;AACX,QAAA;AACY,UAAA;AAC5B,QAAA;AACF,MAAA;AACO,MAAA;AACR,IAAA;AACH,EAAA;AAE8B,EAAA;AACG,IAAA;AACjC,EAAA;AAE0B,EAAA;AACM,IAAA;AACI,IAAA;AACpC,EAAA;AAAA;AAGwE,EAAA;AACxC,IAAA;AACM,IAAA;AACJ,MAAA;AAC9B,MAAA;AACF,IAAA;AAC+B,IAAA;AACjC,EAAA;AAEoE,EAAA;AACvD,IAAA;AACsB,IAAA;AACA,IAAA;AACnC,EAAA;AAEmF,EAAA;AAClE,IAAA;AACO,IAAA;AACf,MAAA;AACwB,QAAA;AACpB,QAAA;AACJ,MAAA;AACwB,QAAA;AACE,QAAA;AACL,QAAA;AACM,QAAA;AACN,QAAA;AACjB,QAAA;AACJ,MAAA;AACwB,QAAA;AACJ,QAAA;AACZ,MAAA;AACgB,QAAA;AACC,QAAA;AACA,QAAA;AACC,QAAA;AACL,QAAA;AACpB,QAAA;AACuB,UAAA;AACzB,QAAA;AACwB,UAAA;AAC1B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACe,MAAA;AACc,QAAA;AACG,QAAA;AACL,QAAA;AACG,QAAA;AACX,QAAA;AACa,QAAA;AACvB,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGsH,EAAA;AACrG,IAAA;AACmB,IAAA;AACD,IAAA;AAED,IAAA;AAC5B,IAAA;AAC6B,MAAA;AACP,MAAA;AACQ,MAAA;AACP,MAAA;AACd,MAAA;AACqB,MAAA;AACH,MAAA;AACb,QAAA;AACD,QAAA;AACQ,QAAA;AACJ,QAAA;AACW,UAAA;AACD,UAAA;AACE,UAAA;AAC7B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AAC0B,MAAA;AACF,MAAA;AACL,MAAA;AACL,IAAA;AACkB,MAAA;AACR,MAAA;AAClB,MAAA;AACR,IAAA;AACF,EAAA;AACF;AnBwwKsC;AACA;AoBp+KH;AACE;AACD;AACD;AAE4C;AACtE,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACO,MAAA;AAC/B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACD,UAAA;AACa,UAAA;AACG,UAAA;AACJ,UAAA;AACA,UAAA;AACG,UAAA;AACI,UAAA;AACH,UAAA;AACE,UAAA;AAC7B,QAAA;AACe,QAAA;AACO,QAAA;AACxB,MAAA;AACsB,MAAA;AACM,MAAA;AAC9B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACD,UAAA;AACa,UAAA;AACM,UAAA;AAC9B,QAAA;AACsB,QAAA;AACA,QAAA;AACxB,MAAA;AACsB,MAAA;AACO,MAAA;AAC/B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACU,MAAA;AAClC,IAAA;AACF,EAAA;AACF;ApBq+KsC;AACA;AqBliLH;AACE;AACF;AACC;AACJ;AACA;AAClB;AACN,EAAA;AAC6B,EAAA;AAChB,EAAA;AACG,EAAA;AACxB;AACa;AACL,EAAA;AACO,EAAA;AACD,EAAA;AACwB,IAAA;AACA,IAAA;AACT,IAAA;AACC,IAAA;AAC5B,EAAA;AAC8B,EAAA;AACR,EAAA;AACxB;AAC8B;AAEgC;AACP;AAEsB;AACzC,EAAA;AACT,EAAA;AACQ,IAAA;AACH,IAAA;AACG,IAAA;AACF,IAAA;AACI,IAAA;AACP,IAAA;AACI,IAAA;AACF,IAAA;AACI,IAAA;AACF,IAAA;AACL,IAAA;AACE,IAAA;AACM,IAAA;AACN,IAAA;AAC7B,EAAA;AACO,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACI,QAAA;AACS,QAAA;AAC5B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACc,UAAA;AACJ,UAAA;AACQ,UAAA;AACV,UAAA;AACS,UAAA;AAC7B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACN,MAAA;AACW,QAAA;AACF,QAAA;AACzB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACkB,UAAA;AACH,UAAA;AACA,UAAA;AAC3B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACJ,MAAA;AACpB,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACL,UAAA;AACC,UAAA;AACqB,UAAA;AACL,UAAA;AACxB,QAAA;AACgB,QAAA;AACM,QAAA;AACxB,MAAA;AACsB,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACC,UAAA;AACN,UAAA;AAChB,UAAA;AACR,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACO,QAAA;AACG,QAAA;AACzB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACL,UAAA;AACmB,UAAA;AACH,UAAA;AACA,UAAA;AACC,UAAA;AACxB,QAAA;AACgB,QAAA;AACM,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACY,UAAA;AACE,UAAA;AACH,UAAA;AACA,UAAA;AACC,UAAA;AACM,UAAA;AAC9B,QAAA;AACe,QAAA;AACO,QAAA;AACxB,MAAA;AACsB,MAAA;AACK,MAAA;AAC7B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACD,MAAA;AACM,QAAA;AACJ,QAAA;AACvB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACY,UAAA;AACD,UAAA;AACA,UAAA;AACH,UAAA;AACpB,QAAA;AACyB,QAAA;AACH,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACb,QAAA;AACc,QAAA;AACf,QAAA;AACjB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACc,UAAA;AACL,UAAA;AACM,UAAA;AACC,UAAA;AAC5B,QAAA;AACiB,QAAA;AACK,QAAA;AACxB,MAAA;AACsB,MAAA;AACU,MAAA;AAClC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACQ,MAAA;AAChC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACS,UAAA;AACP,UAAA;AACY,UAAA;AAC1B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACE,QAAA;AACV,QAAA;AACvB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACK,MAAA;AAC7B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACF,UAAA;AACA,YAAA;AACI,YAAA;AACA,YAAA;AACG,YAAA;AACN,YAAA;AACC,cAAA;AACa,cAAA;AACA,cAAA;AACG,cAAA;AACxB,YAAA;AACF,UAAA;AACwB,UAAA;AACL,UAAA;AACK,UAAA;AAC1B,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACW,UAAA;AACA,UAAA;AACM,UAAA;AACT,UAAA;AACpB,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACI,QAAA;AACM,QAAA;AACzB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACF,UAAA;AACA,YAAA;AACI,YAAA;AACA,YAAA;AACG,YAAA;AACN,YAAA;AACC,cAAA;AACM,cAAA;AACS,gBAAA;AACJ,gBAAA;AACjB,cAAA;AACoB,cAAA;AACE,cAAA;AACxB,YAAA;AACF,UAAA;AACqB,UAAA;AACA,UAAA;AACK,UAAA;AACC,UAAA;AACD,UAAA;AACR,UAAA;AACpB,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACG,MAAA;AAC3B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACC,MAAA;AACzB,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACC,MAAA;AACzB,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACS,MAAA;AACP,MAAA;AACC,QAAA;AACP,QAAA;AACI,UAAA;AACgB,YAAA;AACD,YAAA;AACxB,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AAC6B,MAAA;AACT,MAAA;AAC8D,MAAA;AACtF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACU,MAAA;AAClC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACG,MAAA;AAC3B,IAAA;AACF,EAAA;AACF;ArB2hLsC;AACA;AsB99LH;AACE;AACF;AACH;AACG;AACtB;AACL,EAAA;AACO,EAAA;AACD,EAAA;AACwB,IAAA;AACA,IAAA;AACT,IAAA;AACC,IAAA;AAC5B,EAAA;AAC8B,EAAA;AACR,EAAA;AACxB;AAE+C;AAEf;AACd,EAAA;AACE,EAAA;AACE,EAAA;AACA,EAAA;AACF,EAAA;AACL,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACd;AAE2E;AACrC,EAAA;AAC7B,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACWC,MAAAA;AACS,MAAA;AACI,MAAA;AAC5B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACgB,UAAA;AACC,UAAA;AAC7B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACG,MAAA;AACP,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AAC6B,MAAA;AACT,MAAA;AACU,MAAA;AACd,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACiBC,QAAAA;AACD,QAAA;AACxB,MAAA;AACsB,MAAA;AACK,MAAA;AACT,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AAC6B,MAAA;AACT,MAAA;AACQ,MAAA;AACZ,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACDA,UAAAA;AACe,UAAA;AACT,UAAA;AACU,UAAA;AACjB,UAAA;AACA,YAAA;AACO,YAAA;AACY,YAAA;AACN,YAAA;AACG,YAAA;AACxB,UAAA;AACmB,UAAA;AACM,UAAA;AACA,UAAA;AAC3B,QAAA;AACiB,QAAA;AACK,QAAA;AACxB,MAAA;AACsB,MAAA;AACQ,MAAA;AACZ,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACe,UAAA;AACN,UAAA;AACrB,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACG,MAAA;AACP,QAAA;AACC,QAAA;AACnB,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACA,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACF,UAAA;AAC3B,QAAA;AACmB,QAAA;AACG,QAAA;AACxB,MAAA;AACsB,MAAA;AACU,MAAA;AACT,QAAA;AACM,QAAA;AACJ,QAAA;AACI,QAAA;AAC7B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACa,UAAA;AACL,UAAA;AACpB,QAAA;AACgB,QAAA;AACM,QAAA;AACxB,MAAA;AACsB,MAAA;AACU,MAAA;AAClC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACO,MAAA;AAC/B,IAAA;AACF,EAAA;AACF;AtBq9LsC;AACA;AuBlqMH;AACA;AACE;AACF;AAE0C;AACpE,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACR,UAAA;AACO,UAAA;AAC5B,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACjC,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACO,MAAA;AAC/B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACM,MAAA;AAC9B,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AACM,MAAA;AACkB,MAAA;AACT,MAAA;AACE,MAAA;AAC1B,IAAA;AACF,EAAA;AACF;AvBkqMsC;AACA;AwBltMH;AAE2C;AACrE,EAAA;AACL,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACiB,UAAA;AACJ,UAAA;AACK,UAAA;AACF,UAAA;AACA,UAAA;AACH,UAAA;AACzB,QAAA;AACsB,QAAA;AACxB,MAAA;AACsB,MAAA;AACA,MAAA;AACJ,QAAA;AACa,QAAA;AACD,QAAA;AACD,QAAA;AACd,QAAA;AACO,UAAA;AACV,UAAA;AACE,UAAA;AACC,UAAA;AACI,UAAA;AACG,UAAA;AACR,UAAA;AACc,UAAA;AAC1B,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACF,IAAA;AACA,IAAA;AACQ,MAAA;AACC,MAAA;AAEL,MAAA;AACW,MAAA;AACL,QAAA;AACM,QAAA;AACe,UAAA;AACC,UAAA;AAC5B,QAAA;AACkB,QAAA;AACI,QAAA;AACxB,MAAA;AACsB,MAAA;AACS,MAAA;AACb,QAAA;AACI,QAAA;AACQ,QAAA;AACT,QAAA;AACrB,MAAA;AACF,IAAA;AACF,EAAA;AACF;AxBitMsC;AACA;AyB/vMH;AACE;AACF;AACC;AAEJ;AACA;AACG;AAgBvB;AACJ,EAAA;AACO,EAAA;AACD,EAAA;AACuB,IAAA;AACL,IAAA;AACD,IAAA;AACO,IAAA;AACL,IAAA;AACF,IAAA;AACM,IAAA;AACJ,IAAA;AACI,IAAA;AACR,IAAA;AAClB,IAAA;AACC,MAAA;AACyB,MAAA;AAClB,MAAA;AACf,IAAA;AACF,EAAA;AACsB,EAAA;AACxB;AAEiB;AACT,EAAA;AACO,EAAA;AACD,EAAA;AACiB,IAAA;AACK,IAAA;AACJ,IAAA;AACK,IAAA;AACF,IAAA;AACC,IAAA;AACP,IAAA;AACK,IAAA;AACC,IAAA;AACE,IAAA;AACN,IAAA;AAC7B,EAAA;AACsB,EAAA;AACxB;AAEc;AACN,EAAA;AACO,EAAA;AACD,EAAA;AACsB,IAAA;AACC,IAAA;AACD,IAAA;AACR,IAAA;AACC,IAAA;AACQ,IAAA;AACnC,EAAA;AACsB,EAAA;AACxB;AAEwC;AAEvB;AACT,EAAA;AAEJ,EAAA;AACU,EAAA;AACgB,IAAA;AACM,IAAA;AACN,IAAA;AAClB,IAAA;AACI,IAAA;AACJ,MAAA;AACO,MAAA;AACD,MAAA;AACoB,QAAA;AACL,QAAA;AACA,QAAA;AACE,QAAA;AACC,QAAA;AAC9B,MAAA;AACsB,MAAA;AACxB,IAAA;AACF,EAAA;AACsB,EAAA;AACxB;AAEoB;AACL,EAAA;AACuB,EAAA;AACtC;AAEgB;AACR,EAAA;AACO,EAAA;AACD,EAAA;AACkB,IAAA;AACA,IAAA;AACpB,IAAA;AACV,EAAA;AACsB,EAAA;AACxB;AAEe;AACP,EAAA;AACO,EAAA;AACD,EAAA;AACkB,IAAA;AACF,IAAA;AACQ,IAAA;AACpC,EAAA;AACsB,EAAA;AACxB;AAEkB;AACV,EAAA;AACO,EAAA;AACD,EAAA;AACJ,IAAA;AACE,MAAA;AACa,MAAA;AAEjB,MAAA;AACJ,IAAA;AACiC,IAAA;AACA,IAAA;AACN,IAAA;AACO,IAAA;AACP,IAAA;AACM,IAAA;AACL,IAAA;AACM,IAAA;AACpC,EAAA;AACsB,EAAA;AACxB;AAEsB;AACd,EAAA;AACO,EAAA;AACD,EAAA;AACJ,IAAA;AACE,MAAA;AACiB,MAAA;AACV,MAAA;AACf,IAAA;AAC6B,IAAA;AACK,IAAA;AACN,IAAA;AACK,IAAA;AACN,IAAA;AACM,IAAA;AACnC,EAAA;AACsB,EAAA;AACxB;AAEuB;AACf,EAAA;AACO,EAAA;AACD,EAAA;AACwB,IAAA;AACD,IAAA;AACP,IAAA;AACO,IAAA;AACP,IAAA;AACG,IAAA;AACF,IAAA;AACO,IAAA;AACP,IAAA;AACM,IAAA;AACL,IAAA;AAC9B,EAAA;AACsB,EAAA;AACxB;AAEsB;AACd,EAAA;AACO,EAAA;AACD,EAAA;AACgB,IAAA;AACG,IAAA;AACK,IAAA;AACL,IAAA;AACI,IAAA;AACP,IAAA;AAC5B,EAAA;AACsB,EAAA;AACxB;AAEsB;AACd,EAAA;AACO,EAAA;AACD,EAAA;AACe,IAAA;AACE,IAAA;AACG,IAAA;AACI,IAAA;AACP,IAAA;AACI,IAAA;AACjC,EAAA;AACsB,EAAA;AACxB;AAEqB;AACgB,EAAA;AACA,EAAA;AAC3B,EAAA;AACA,IAAA;AACuB,IAAA;AAChB,IAAA;AACf,EAAA;AACK,EAAA;AAC6B,EAAA;AACP,EAAA;AACQ,EAAA;AACzB,EAAA;AACH,EAAA;AACwB,EAAA;AACrB,EAAA;AACD,EAAA;AACD,EAAA;AACH,EAAA;AACI,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACF,EAAA;AACT;AAEe;AACP,EAAA;AACI,EAAA;AACA,EAAA;AACG,EAAA;AACN,EAAA;AACC,IAAA;AACM,IAAA;AACW,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACI,MAAA;AACA,MAAA;AACG,MAAA;AACD,MAAA;AACF,MAAA;AAC3B,IAAA;AACmB,IAAA;AACG,IAAA;AACxB,EAAA;AACF;AAEqC;AAC4B,EAAA;AAClC,EAAA;AAC/B;AAEqC;AAC5B,EAAA;AACiB,IAAA;AAChB,IAAA;AACE,IAAA;AACI,IAAA;AACF,IAAA;AACE,IAAA;AACH,IAAA;AACG,IAAA;AACc,IAAA;AACP,IAAA;AACD,IAAA;AACgB,IAAA;AACP,IAAA;AACI,IAAA;AACL,IAAA;AAC5B,EAAA;AACF;AAGgB;AACJ,EAAA;AACkB,EAAA;AACc,EAAA;AAEQ,EAAA;AAClB,IAAA;AACJ,IAAA;AACO,IAAA;AACxB,IAAA;AACX,EAAA;AAII,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWD,IAAAA;AACS,IAAA;AACP,IAAA;AACQ,MAAA;AACT,MAAA;AACL,MAAA;AACW,QAAA;AACC,QAAA;AACH,QAAA;AACW,QAAA;AACR,QAAA;AACU,QAAA;AACV,QAAA;AACR,QAAA;AACS,QAAA;AACS,QAAA;AACnB,QAAA;AACW,QAAA;AACX,QAAA;AACA,QAAA;AACqB,QAAA;AAClB,QAAA;AACa,QAAA;AAC1B,MAAA;AACF,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACC,QAAA;AACJ,QAAA;AACE,QAAA;AAC3B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACN,IAAA;AACiB,MAAA;AACC,MAAA;AAClC,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACC,QAAA;AACJ,QAAA;AACzB,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACQ,IAAA;AAC/B,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWA,IAAAA;AACS,IAAA;AACN,IAAA;AACa,MAAA;AACC,MAAA;AACA,MAAA;AAC9B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACAA,IAAAA;AACS,IAAA;AACS,IAAA;AAChC,EAAA;AAEc,EAAA;AACP,IAAA;AACM,IAAA;AACkB,MAAA;AACD,MAAA;AAC7B,IAAA;AACsB,IAAA;AACxB,EAAA;AAEI,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACP,QAAA;AACM,QAAA;AACA,QAAA;AACA,QAAA;AACE,QAAA;AACD,QAAA;AACC,QAAA;AACT,QAAA;AACA,QAAA;AACY,QAAA;AACR,QAAA;AACO,QAAA;AACD,QAAA;AACJ,QAAA;AAC1B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AAC7B,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWA,IAAAA;AACS,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACc,QAAA;AACM,QAAA;AACH,QAAA;AACC,QAAA;AAC9B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACO,IAAA;AAC9B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACiB,QAAA;AACH,QAAA;AACI,QAAA;AACD,QAAA;AACpB,QAAA;AACC,UAAA;AACqB,UAAA;AACd,UAAA;AACf,QAAA;AAC4B,QAAA;AACrB,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AACM,YAAA;AACM,YAAA;AACJ,YAAA;AACM,YAAA;AAC1B,UAAA;AACsB,UAAA;AACxB,QAAA;AACF,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACsB,IAAA;AACY,IAAA;AACnC,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWA,IAAAA;AACS,IAAA;AACN,IAAA;AACiB,MAAA;AACvB,QAAA;AACG,QAAA;AACY,QAAA;AACC,QAAA;AACK,QAAA;AAC3B,MAAA;AACJ,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACI,IAAA;AACA,MAAA;AACC,MAAA;AAC3B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACiB,QAAA;AACD,QAAA;AACG,QAAA;AAC/B,MAAA;AAC6B,MAAA;AACP,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACDC,QAAAA;AACU,QAAA;AACQ,QAAA;AACF,QAAA;AACG,QAAA;AACH,QAAA;AAC3B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWD,IAAAA;AACS,IAAA;AACP,IAAA;AACQ,MAAA;AACW,MAAA;AAClC,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AAC6B,IAAA;AACT,IAAA;AACD,IAAA;AACG,MAAA;AACY,MAAA;AACX,MAAA;AACC,MAAA;AACI,MAAA;AAC9B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AAC6B,IAAA;AACT,IAAA;AACQ,IAAA;AAC/B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACY,QAAA;AACE,QAAA;AAC1B,MAAA;AACe,MAAA;AACO,MAAA;AACxB,IAAA;AACsB,IAAA;AACK,IAAA;AAC5B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACD,IAAA;AACH,MAAA;AACK,MAAA;AACvB,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACqB,MAAA;AACL,MAAA;AACxB,IAAA;AACsB,IAAA;AACE,IAAA;AACzB,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACiB,QAAA;AACC,QAAA;AAC9B,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACsB,IAAA;AACO,IAAA;AAC9B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACY,QAAA;AACI,QAAA;AACC,QAAA;AACR,QAAA;AACrB,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACS,IAAA;AACF,MAAA;AACpB,MAAA;AACI,QAAA;AACgB,UAAA;AACD,UAAA;AACxB,QAAA;AACF,MAAA;AACF,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACoBE,MAAAA;AACJ,MAAA;AACxB,IAAA;AACsB,IAAA;AACY,IAAA;AACL,MAAA;AACO,MAAA;AACb,MAAA;AACI,MAAA;AAC3B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AAC6B,IAAA;AACT,IAAA;AACE,IAAA;AACI,MAAA;AACL,MAAA;AACO,MAAA;AAC9B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACW,QAAA;AACA,QAAA;AACM,QAAA;AAC7B,MAAA;AACmB,MAAA;AACG,MAAA;AACxB,IAAA;AACsB,IAAA;AACY,IAAA;AACnC,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWF,IAAAA;AACS,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACE,IAAA;AACzB,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACW,IAAA;AAClC,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACF,QAAA;AACCC,QAAAA;AACkB,QAAA;AACF,QAAA;AACN,QAAA;AACP,QAAA;AACa,QAAA;AACE,QAAA;AAC7B,MAAA;AACmB,MAAA;AACG,MAAA;AACxB,IAAA;AACsB,IAAA;AACA,IAAA;AACJ,MAAA;AACW,MAAA;AACC,MAAA;AACC,MAAA;AAC/B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACmB,QAAA;AACJ,QAAA;AAC3B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACM,IAAA;AACD,MAAA;AACO,MAAA;AAC5B,MAAA;AAAgC,QAAA;AAAkH,MAAA;AAC/I,MAAA;AACM,QAAA;AACgB,QAAA;AACA,UAAA;AACA,UAAA;AACD,UAAA;AACjB,UAAA;AACC,YAAA;AACK,YAAA;AACM,YAAA;AACE,YAAA;AACV,YAAA;AACgB,YAAA;AACjB,YAAA;AACU,YAAA;AACM,YAAA;AAC1B,UAAA;AACD,QAAA;AACH,MAAA;AACF,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACkB,QAAA;AACN,QAAA;AACA,QAAA;AACG,QAAA;AACC,QAAA;AACA,QAAA;AAClB,QAAA;AACA,UAAA;AACO,UAAA;AACY,UAAA;AACN,UAAA;AACG,UAAA;AACxB,QAAA;AACuB,QAAA;AACJ,QAAA;AACQ,QAAA;AACA,QAAA;AACA,QAAA;AAC7B,MAAA;AACsB,MAAA;AACA,MAAA;AACxB,IAAA;AACsB,IAAA;AACN,IAAA;AACC,MAAA;AACc,MAAA;AAC/B,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACc,QAAA;AACI,QAAA;AACD,QAAA;AACD,QAAA;AAC5B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACQ,IAAA;AACL,MAAA;AACL,MAAA;AACpB,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACY,IAAA;AACnC,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACAD,IAAAA;AACoBG,IAAAA;AAClB,IAAA;AACS,MAAA;AACD,MAAA;AACQ,MAAA;AAC/B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACAH,IAAAA;AACoBG,IAAAA;AAClB,IAAA;AACS,MAAA;AACD,MAAA;AACQ,MAAA;AAC/B,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACkB,QAAA;AACD,QAAA;AACD,QAAA;AAC5B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACS,IAAA;AACzB,MAAA;AAAqC,QAAA;AAA2F,MAAA;AACrG,MAAA;AACJ,MAAA;AAC7B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACgB,QAAA;AACC,QAAA;AACF,QAAA;AACC,QAAA;AAC5B,MAAA;AACsB,MAAA;AACxB,IAAA;AACsB,IAAA;AACI,IAAA;AACF,MAAA;AACO,MAAA;AAC/B,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AAEL,IAAA;AACWH,IAAAA;AACS,IAAA;AACP,IAAA;AACS,MAAA;AACS,MAAA;AACjC,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACM,IAAA;AAC7B,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACM,IAAA;AACkB,IAAA;AACT,IAAA;AACK,IAAA;AACG,MAAA;AAChB,MAAA;AACoB,MAAA;AAClC,IAAA;AACD,EAAA;AAEmC,EAAA;AACA,EAAA;AACA,EAAA;AACtB,EAAA;AACA,EAAA;AACoB,EAAA;AAEP,EAAA;AAC7B;AzBgpMsC;AACA;A0BxrOmD;AACnD,EAAA;AACT,EAAA;AACS,EAAA;AACb,EAAA;AACzB;AAY0B;AAatB,EAAA;AAFiB,IAAA;AACA,IAAA;AAChB,EAAA;AAFgB,EAAA;AACA,EAAA;AAXD,oBAAA;AAAA;AAEK,oBAAA;AAAA;AAEE,oBAAA;AACJ,oBAAA;AACU,oBAAA;AACb,oBAAA;AAO2C,EAAA;AACxC,IAAA;AACO,IAAA;AACQ,IAAA;AACpC,EAAA;AAE4C,EAAA;AACtB,IAAA;AACa,IAAA;AACnC,EAAA;AAAA;AAGoC,EAAA;AACrB,IAAA;AACS,IAAA;AACO,IAAA;AACZ,MAAA;AACiB,MAAA;AACL,IAAA;AACpB,MAAA;AACT,IAAA;AAC4B,IAAA;AACE,IAAA;AACV,IAAA;AACS,MAAA;AACpB,MAAA;AACT,IAAA;AACuB,IAAA;AACF,MAAA;AACW,MAAA;AACvB,MAAA;AACT,IAAA;AAC+B,IAAA;AAClB,MAAA;AACJ,MAAA;AACT,IAAA;AACI,IAAA;AACkB,MAAA;AACY,MAAA;AAChB,MAAA;AACc,MAAA;AACvB,MAAA;AACK,IAAA;AACgB,MAAA;AACrB,MAAA;AACT,IAAA;AACF,EAAA;AAEyC,EAAA;AACnB,IAAA;AACW,IAAA;AACE,IAAA;AACH,IAAA;AAChC,EAAA;AAEiC,EAAA;AACG,IAAA;AACnB,IAAA;AACa,IAAA;AACN,IAAA;AACW,MAAA;AAC/B,MAAA;AACF,IAAA;AACyB,IAAA;AACJ,MAAA;AACI,MAAA;AACM,MAAA;AACE,MAAA;AACL,MAAA;AAC1B,MAAA;AACF,IAAA;AACgC,IAAA;AAClB,IAAA;AACU,IAAA;AACM,IAAA;AACH,IAAA;AACK,IAAA;AAClC,EAAA;AAE+B,EAAA;AACK,IAAA;AACnB,IAAA;AACa,IAAA;AAChB,IAAA;AACM,IAAA;AACY,IAAA;AAChC,EAAA;AAEe,EAAA;AACE,IAAA;AACG,IAAA;AACgB,IAAA;AACtB,IAAA;AACd,EAAA;AAEsB,EAAA;AACC,IAAA;AACvB,EAAA;AAAA;AAGyF,EAAA;AAC7D,IAAA;AACV,IAAA;AACY,MAAA;AAC1B,MAAA;AACF,IAAA;AACmB,IAAA;AACa,IAAA;AACR,IAAA;AAEU,IAAA;AACD,IAAA;AACD,IAAA;AAClC,EAAA;AACF;AAIE;AAI2B,EAAA;AACG,EAAA;AACN,EAAA;AACI,IAAA;AACM,IAAA;AACX,IAAA;AACd,IAAA;AACT,EAAA;AACwB,EAAA;AACN,IAAA;AACT,IAAA;AACT,EAAA;AACgB,EAAA;AACe,EAAA;AACrB,EAAA;AACY,IAAA;AACb,IAAA;AACT,EAAA;AACoC,EAAA;AACJ,EAAA;AACA,IAAA;AACD,IAAA;AACtB,IAAA;AACT,EAAA;AACiB,EAAA;AACO,IAAA;AACf,IAAA;AACT,EAAA;AACiB,EAAA;AACe,IAAA;AACvB,IAAA;AACT,EAAA;AACsB,EAAA;AACC,IAAA;AACd,IAAA;AACT,EAAA;AACO,EAAA;AACT;AASkC;AAKgC,EAAA;AAAnC,IAAA;AAAoC,EAAA;AAApC,EAAA;AAJkC,oBAAA;AACT,oBAAA;AACvC,oBAAA;AAAA;AAKqB,EAAA;AACH,IAAA;AACA,IAAA;AACH,IAAA;AACJ,IAAA;AACV,MAAA;AACQ,MAAA;AACZ,MAAA;AACV,IAAA;AACY,IAAA;AACkC,IAAA;AACf,IAAA;AACN,MAAA;AACQ,MAAA;AACJ,MAAA;AACH,MAAA;AACE,MAAA;AACV,MAAA;AACA,MAAA;AACwB,MAAA;AACxC,MAAA;AACU,IAAA;AACL,IAAA;AACT,EAAA;AACF;AAG4J;AAC5H,EAAA;AACN,EAAA;AACiF,EAAA;AACrE,EAAA;AACjB,IAAA;AACa,IAAA;AACD,IAAA;AAC/B,EAAA;AACyB,EAAA;AACS,EAAA;AAC3B,EAAA;AACT;AAWuD;AAClB,EAAA;AACL,EAAA;AACK,EAAA;AACJ,EAAA;AACD,EAAA;AACE,EAAA;AACJ,EAAA;AACK,EAAA;AACD,EAAA;AACI,EAAA;AACD,EAAA;AACC,EAAA;AACH,EAAA;AACnC;AAEuD;AAC7B,EAAA;AACA,EAAA;AACE,EAAA;AACK,EAAA;AACF,EAAA;AACE,EAAA;AACH,EAAA;AACD,EAAA;AAC7B;AAG6D;AAC1C,EAAA;AACgB,EAAA;AACE,EAAA;AACrC;AAG8G;AACzE,EAAA;AACA,EAAA;AAC8B,EAAA;AACnE;AAGkE;AAC3B,EAAA;AACH,EAAA;AACpC;AAGyD;AACpB,EAAA;AACrC;AAM+C;AACV,EAAA;AAClB,EAAA;AACc,EAAA;AACE,IAAA;AACR,MAAA;AACA,MAAA;AACU,MAAA;AACP,MAAA;AACM,MAAA;AACH,MAAA;AAEG,MAAA;AACpB,MAAA;AACI,MAAA;AACA,MAAA;AACiB,MAAA;AACjC,IAAA;AACF,EAAA;AACoC,EAAA;AACtC;AAGqE;AACnC,EAAA;AACC,EAAA;AACH,EAAA;AACC,EAAA;AACjC;AAa4D;AAC9B,EAAA;AACH,EAAA;AAC3B;AAyBiD;AACb,EAAA;AACH,EAAA;AACjC;AASiI;AACzG,EAAA;AACf,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACmB,UAAA;AACD,UAAA;AACE,UAAA;AACnB,UAAA;AACoB,UAAA;AACF,UAAA;AACE,UAAA;AACH,UAAA;AACjB,UAAA;AACV,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACS,QAAA;AACjB,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACkB,UAAA;AACjB,UAAA;AACiB,UAAA;AACjB,UAAA;AACgB,UAAA;AACE,UAAA;AAC5B,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACR,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACoB,UAAA;AACnB,UAAA;AACA,UAAA;AACmB,UAAA;AACnB,UAAA;AACV,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACS,QAAA;AACjB,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACC,UAAA;AACiB,UAAA;AACG,UAAA;AACJ,UAAA;AACG,UAAA;AACH,UAAA;AACrB,UAAA;AACL,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACO,QAAA;AACf,MAAA;AACU,IAAA;AACsB,MAAA;AAErB,MAAA;AAEyB,QAAA;AACE,QAAA;AACH,QAAA;AACC,QAAA;AACD,QAAA;AACG,QAAA;AAC3B,QAAA;AAGH,MAAA;AAC8B,QAAA;AACA,QAAA;AAEL,MAAA;AACJ,MAAA;AAC/B,IAAA;AACK,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACqB,UAAA;AACA,UAAA;AAC9B,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACR,MAAA;AACG,IAAA;AACI,MAAA;AACE,QAAA;AACE,QAAA;AACoB,UAAA;AACF,UAAA;AAC3B,QAAA;AACY,QAAA;AACL,QAAA;AACK,QAAA;AACN,QAAA;AACR,MAAA;AACG,IAAA;AAC4B,MAAA;AAC5B,IAAA;AACa,MAAA;AAClB,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAG0E;AAC5C,EAAA;AACQ,EAAA;AACH,EAAA;AACnC;AAGiE;AACwB,EAAA;AAC7D,IAAA;AACM,IAAA;AACII,IAAAA;AACpC,EAAA;AACmC,EAAA;AACA,EAAA;AAC0D,EAAA;AAC/F;AAG+D;AAC/B,EAAA;AACM,EAAA;AACtC;AAGkE;AAGZ;AACrB,EAAA;AACjC;AAOkE;AACzD,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACC,EAAA;AACC,EAAA;AACF,EAAA;AACR;AAG2D;AAC7B,EAAA;AACE,EAAA;AACE,EAAA;AACF,EAAA;AACI,EAAA;AACV,EAAA;AACQ,EAAA;AACI,EAAA;AACV,EAAA;AAC5B;AAG0D;AACxB,EAAA;AACJ,EAAA;AAC9B;AAGuD;AAGyB;AACtD,EAAA;AACU,EAAA;AACpC;AAG2D;AACvB,EAAA;AACjB,EAAA;AACV,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACwB,MAAA;AACxB,IAAA;AACU,MAAA;AACf,IAAA;AACqB,MAAA;AACvB,EAAA;AACF;AAOiE;AACrD,EAAA;AACa,EAAA;AACC,EAAA;AACD,EAAA;AAAC,EAAA;AACV,EAAA;AACL,EAAA;AACmB,EAAA;AACb,IAAA;AACqB,IAAA;AAAE,IAAA;AACvB,IAAA;AACf,EAAA;AACa,EAAA;AACA,EAAA;AACD,IAAA;AACF,oBAAA;AACV,EAAA;AACF;AAMoE;AACxC,EAAA;AACO,EAAA;AACF,EAAA;AACN,EAAA;AACC,EAAA;AACM,EAAA;AACT,EAAA;AACF,IAAA;AACmB,IAAA;AACxC,EAAA;AACa,EAAA;AACf;AAGwF;AAGL;AAG5B;AAC3B,EAAA;AACE,EAAA;AACO,EAAA;AACrC;AAGsF;AACC;AAGR;AAGU;AACrD,EAAA;AACA,EAAA;AACpC;AAGyD;AACnB,EAAA;AACT,EAAA;AACD,EAAA;AAC5B;AAG+I;AAC7G,EAAA;AAClB,EAAA;AACP,IAAA;AAC6B,MAAA;AAC7B,IAAA;AAC6B,MAAA;AAC7B,IAAA;AAC4B,MAAA;AAC5B,IAAA;AAC4B,MAAA;AACnC,EAAA;AACF;A1BwhOsC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/paint-editor/index.cjs","sourcesContent":[null,"/**\n * The paint editor's document: a canvas (size and resolution), a paper, and the shared raster\n * layer stack (`@pixerate/editor/raster`: pixel layers and groups with masks, clipping, locks and\n * blend modes). Layers are immutable and share tiles, so a history entry is a reference to a\n * whole document state.\n *\n * Saved documents are JSON plus PNG assets, through the image editor's layer serializer (so the\n * same layer tree loads in both editors), with the paper as plain settings.\n */\n\nimport type { RasterGroupLayer, RasterLayer, RasterPixelLayer } from '../raster/layers';\nimport { emptySurface } from '../raster/surface';\nimport { normalizePaper, type PaperSettings } from './paper';\nimport type { RecordedStroke } from './strokes';\nimport type { WetEvent } from './wet';\nimport type { ThickChannels } from './thick';\nimport type { Sticker } from './stickers';\n\nexport type PaintPixelLayer = RasterPixelLayer;\nexport type PaintGroupLayer = RasterGroupLayer<PaintLayer>;\n/** A layer of the paint editor: the shared stack's pixel layers and groups. */\nexport type PaintLayer = RasterLayer;\n\n/** Canvas size limits (pixels per side). */\nexport const MIN_CANVAS_SIDE = 1;\nexport const MAX_CANVAS_SIDE = 16384;\nexport const DEFAULT_DPI = 300;\n\nexport interface PaintCanvas {\n width: number;\n height: number;\n /** Pixels per inch, for print sizes and PSD resolution (default 300). */\n dpi: number;\n}\n\n/** Everything one undo step restores. */\nexport interface PaintDocumentState {\n canvas: PaintCanvas;\n paper: PaperSettings;\n /** Bottom to top. */\n layers: PaintLayer[];\n activeLayerId: string | null;\n /** Every committed stroke, oldest first (strokes are data: replay, vary, time-lapse later). */\n strokes: RecordedStroke[];\n /** Wet-media events other than strokes (drying, tilt, settings), with the clock they happened at. */\n wetEvents?: WetEvent[];\n /** Changes besides strokes that a replay repeats (fills, clears, flattened relief), in order. */\n paintEvents?: PaintEvent[];\n /** Sticker layers: each layer's stickers, bottom first (its pixels are drawn from them). */\n stickers?: Readonly<Record<string, Sticker[]>>;\n /** Thick paint: each layer's height and material channels (layers without relief are missing). */\n thick?: Readonly<Record<string, ThickChannels>>;\n}\n\n/** A change a replay repeats besides strokes, recorded with how many strokes came before it. */\nexport interface PaintEvent {\n at: number;\n kind: 'fill' | 'clear' | 'flatten' | 'stickers';\n layerId: string;\n /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers after an edit. */\n data?: Record<string, unknown>;\n}\n\nlet nextId = 1;\n\n/** A new layer id, unique in this session. */\nexport function newPaintLayerId(): string {\n return `pl${Date.now().toString(36)}${(nextId++).toString(36)}`;\n}\n\n/** An empty, visible, normal pixel layer. */\nexport function createPaintLayer(name: string, id = newPaintLayerId()): PaintPixelLayer {\n return { type: 'pixel', id, name, visible: true, opacity: 1, blend: 'normal', surface: emptySurface(4), offset: { x: 0, y: 0 } };\n}\n\nexport class PaintDocumentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PaintDocumentError';\n }\n}\n\n/** Validates and rounds a canvas size; throws a `PaintDocumentError` that says what is wrong. */\nexport function normalizeCanvas(raw: Partial<PaintCanvas>): PaintCanvas {\n const side = (v: unknown, what: string) => {\n if (typeof v !== 'number' || !Number.isFinite(v)) throw new PaintDocumentError(`${what} must be a number of pixels (${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE}).`);\n const n = Math.round(v);\n if (n < MIN_CANVAS_SIDE || n > MAX_CANVAS_SIDE) throw new PaintDocumentError(`${what} ${n} is out of range; use ${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE} pixels.`);\n return n;\n };\n const dpi = raw.dpi === undefined ? DEFAULT_DPI : raw.dpi;\n if (typeof dpi !== 'number' || !Number.isFinite(dpi) || dpi < 1 || dpi > 10000) throw new PaintDocumentError(`dpi ${dpi} is out of range; use 1..10000.`);\n return { width: side(raw.width, 'width'), height: side(raw.height, 'height'), dpi: Math.round(dpi * 100) / 100 };\n}\n\n/** A blank document: one empty layer over the paper. */\nexport function createPaintDocument(options: Partial<PaintCanvas> & { paper?: Partial<PaperSettings> } = {}): PaintDocumentState {\n const canvas = normalizeCanvas({ width: options.width ?? 2048, height: options.height ?? 1536, dpi: options.dpi });\n const layer = createPaintLayer('Layer 1');\n return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };\n}\n","/**\n * The paint editor's dab-by-dab stroke engine: every brush whose dabs read what earlier dabs laid\n * down. The Mixer Brush, the blender, the smudge, and dab brushes that pick up paint or are\n * multi-coloured all run here, with the same pen input as dry media (`PenDabWalker`: pressure,\n * tilt, barrel rotation, velocity, rotation modes, jitter), paper grain and symmetry copies.\n *\n * - `mixer` and wet-capable `dab` brushes go through the shared pick-up / mix exchange\n * (`PaintExchange` over a `TileMedium`): each dab picks up from the layer, mixes its paint from\n * the brush colour (or the multi-colour gradient) and the carried paint, and lays it down. A\n * Mixer Brush deposits per dab (photocraft's model); a dab brush keeps the stroke's opacity\n * ceiling: a shared `CoverageMap` builds up as for dry media and each dab lays only the\n * increase, `k = (c₁ − c₀)·o / (1 − c₀·o)`, so a one-colour Normal stroke matches the dry engine.\n * - `blender` calls `blenderDab`, `smudge` a `SmudgeTip` per copy.\n *\n * Pixels go through the session's live tiles (`PaintTileTarget`). Pure data in, pixels out:\n * points, seed, colours and the starting load replay exactly.\n */\n\nimport { CoverageMap, dabRect, rasterizeDab } from '../paint/brush';\nimport { blenderDab, createBlenderScratch, SmudgeTip, type BlenderScratch } from '../paint/blending';\nimport { exchangeRates, PaintExchange, TileMedium, type MixerLoad, type MixingModel, type PaintTileTarget, type RgbTriple } from '../paint/exchange';\nimport { mixerRates } from '../paint/mixer';\nimport { mixPigment8 } from '../raster/blend';\nimport type { RasterRect } from '../raster/surface';\nimport { PaintGradientSampler, type PaintGradientSample, type PaintGradientSource } from '../paint/gradient-paint';\nimport type { MultiColorSettings, PaintBrush } from './brushes';\nimport { IDENTITY_COPY, PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine';\nimport { rand01 } from './noise';\nimport { grainCatch, type PaperGrain } from './paper';\n\nexport interface PaintDabStrokeOptions {\n /** Where the stroke reads and writes pixels. */\n target: PaintTileTarget;\n /** The paint colour, sRGB 0..1. */\n color: RgbTriple;\n /** The secondary colour (a multi-colour brush without its own colours uses both). */\n secondary: RgbTriple;\n /** What the brush carries at the start (mixer and wet-capable dab brushes). */\n load: MixerLoad;\n grain: PaperGrain | null;\n paperDepth: number;\n seed?: number;\n copies?: readonly StrokeCopy[];\n /** Gradient paint (resolved: no `paint` / `secondary` stops): each dab's colour comes from it. */\n paint?: PaintGradientSource | null;\n}\n\n/** Entries in a multi-colour gradient table. */\nconst GRADIENT_STEPS = 32;\n\n/** Colours of a multi-colour brush, resolved against the paint and secondary colours. */\nexport function multiColorPalette(mc: MultiColorSettings, color: RgbTriple, secondary: RgbTriple): RgbTriple[] {\n if (mc.colors.length >= 2) {\n return mc.colors.map((h) => {\n const n = parseInt(h.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n });\n }\n return [[...color], [...secondary]];\n}\n\n/** The blend position `t` between two neighbouring colours at fraction `f`, by softness (0: a hard step at ½). */\nfunction soften(f: number, softness: number): number {\n if (softness >= 1) return f;\n const w = Math.max(1e-3, softness);\n return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));\n}\n\n/** `a` mixed `t` of the way to `b` by the mixing model (8-bit pigment spectra cached). */\nfunction mixRgb(a: RgbTriple, b: RgbTriple, t: number, mixing: MixingModel): RgbTriple {\n if (t <= 0) return [...a];\n if (t >= 1) return [...b];\n if (mixing === 'pigment') {\n const o = [0, 0, 0];\n const q = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255);\n mixPigment8(q(a[0]), q(a[1]), q(a[2]), q(b[0]), q(b[1]), q(b[2]), t, o);\n return [o[0], o[1], o[2]];\n }\n return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n}\n\n/** The tip gradient of a multi-colour brush: `GRADIENT_STEPS` colours across the tip. */\nexport function multiColorGradient(palette: readonly RgbTriple[], softness: number, mixing: MixingModel): RgbTriple[] {\n const n = palette.length;\n const out: RgbTriple[] = [];\n for (let j = 0; j < GRADIENT_STEPS; j++) {\n // Each colour owns an equal band; blends happen across band edges.\n const f = (j / (GRADIENT_STEPS - 1)) * (n - 1);\n const i = Math.min(n - 2, Math.floor(f));\n out.push(mixRgb(palette[i], palette[i + 1], soften(f - i, softness), mixing));\n }\n return out;\n}\n\n/** The colour of dab `index` of a `dab`-mode multi-colour brush. */\nexport function multiColorForDab(mc: MultiColorSettings, palette: readonly RgbTriple[], index: number, seed: number, mixing: MixingModel): RgbTriple {\n const n = palette.length;\n if (mc.order === 'random') {\n const run = Math.floor(index / mc.length);\n return [...palette[Math.min(n - 1, Math.floor(rand01(seed, run, 11) * n))]];\n }\n const f = (index / mc.length) % n;\n const i = Math.floor(f);\n return mixRgb(palette[i], palette[(i + 1) % n], soften(f - i, mc.softness), mixing);\n}\n\ninterface CopyState {\n walker: PenDabWalker;\n exchange: PaintExchange | null;\n smudge: SmudgeTip | null;\n}\n\nexport class PaintDabStroke {\n dabCount = 0;\n private readonly smoother: PenSmoother;\n private readonly copies: readonly StrokeCopy[];\n private readonly states: CopyState[];\n private readonly medium: TileMedium;\n private readonly depth: number;\n private readonly coverage: CoverageMap | null;\n private readonly sampler: PaintGradientSampler | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n private readonly gradient: RgbTriple[] | null;\n /** Opacity across the tip (`across-tip` gradients with opacity stops), or `null`. */\n private readonly gradientAlpha: number[] | null;\n private alphaCov = new Float32Array(0);\n private readonly fieldLut: RgbTriple[] = [];\n private readonly scratch: BlenderScratch | null;\n private readonly mixing: MixingModel;\n private tip = new Float32Array(0);\n private cov = new Float32Array(0);\n private field = new Float32Array(0);\n private readonly paint: RgbTriple = [0, 0, 0];\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n\n constructor(\n readonly brush: PaintBrush,\n private readonly options: PaintDabStrokeOptions\n ) {\n this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);\n this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];\n const exchanging = brush.engine === 'mixer' || brush.engine === 'dab';\n const rates = brush.engine === 'mixer' ? mixerRates(brush.mixer) : exchangeRates(brush.behavior.pickup, brush.behavior.mix, 1, brush.behavior.mixing);\n this.mixing = brush.engine === 'mixer' ? brush.mixer.mixing : brush.behavior.mixing;\n this.states = this.copies.map((c, i) => ({\n walker: new PenDabWalker(brush, ((options.seed ?? 0) + i * 7919) | 0, c.linear),\n exchange: exchanging ? new PaintExchange(rates, options.load) : null,\n smudge: brush.engine === 'smudge' ? new SmudgeTip(brush.smudge, brush.tip.size * (1 + brush.dynamics.tiltSize * 1.5)) : null,\n }));\n // A Mixer Brush mixes as its settings say; a dab brush blends by its tip's blend mode.\n this.medium = new TileMedium(options.target, brush.engine === 'dab' ? { blend: brush.tip.blend } : {});\n this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;\n this.coverage = brush.engine === 'dab' ? new CoverageMap() : null;\n this.sampler = exchanging && options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;\n const table = this.sampler?.acrossTip ? this.sampler.tipTable(GRADIENT_STEPS) : null;\n this.gradient = table ? table.map((e) => e.rgb) : null;\n this.gradientAlpha = table && table.some((e) => e.alpha < 1) ? table.map((e) => e.alpha) : null;\n this.scratch = brush.engine === 'blender' ? createBlenderScratch() : null;\n }\n\n /** What copy 0's brush carries after the dabs so far (mixer and wet-capable dab brushes). */\n get load(): MixerLoad | null {\n return this.states[0].exchange?.load ?? null;\n }\n\n push(points: readonly PenPoint[]): RasterRect[] {\n const rects: RasterRect[] = [];\n for (const raw of points) {\n this.lastRaw = raw;\n const p = this.smoother.push(raw);\n this.lastFed = p;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n this.states[i].walker.push(c.map(p), dabs);\n this.paintDabs(i, dabs, rects);\n });\n }\n return rects;\n }\n\n /** Ends the stroke: the smoothed path catches up with the last raw sample; a tap paints one dab. */\n finish(): RasterRect[] {\n const rects: RasterRect[] = [];\n const raw = this.lastRaw;\n const fed = this.lastFed;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);\n this.states[i].walker.finish(dabs);\n this.paintDabs(i, dabs, rects);\n });\n return rects;\n }\n\n private paintDabs(copy: number, dabs: PenDab[], rects: RasterRect[]): void {\n for (const d of dabs) {\n const rect = dabRect(d, this.brush.tip.hardness);\n if (rect.width <= 0 || rect.height <= 0) continue;\n this.dab(copy, d, rect);\n rects.push(rect);\n this.dabCount++;\n }\n }\n\n /** The tip's pure coverage (`tip`) and, with grain, what the paper catches of it (`cov`). */\n private rasterize(d: PenDab, rect: RasterRect): { tip: Float32Array; cov: Float32Array } {\n const n = rect.width * rect.height;\n if (this.tip.length < n) {\n this.tip = new Float32Array(n);\n this.cov = new Float32Array(n);\n }\n const tip = this.tip.subarray(0, n);\n tip.fill(0);\n rasterizeDab({ ...d, alpha: 1 }, this.brush.tip.hardness, rect, tip);\n if (!(this.depth > 0 && this.options.grain)) return { tip, cov: tip };\n const cov = this.cov.subarray(0, n);\n const grain = this.options.grain;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const y = rect.y + yy + 0.5;\n for (let xx = 0; xx < rect.width; xx++, k++) cov[k] = tip[k] > 0 ? tip[k] * grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure) : 0;\n }\n return { tip, cov };\n }\n\n private dab(copy: number, d: PenDab, rect: RasterRect): void {\n const brush = this.brush;\n const { tip, cov } = this.rasterize(d, rect);\n const st = this.states[copy];\n if (brush.engine === 'blender') {\n blenderDab(this.medium, rect, cov, d.opacity * d.alpha * brush.tip.opacity, d.radius, brush.blender, this.scratch!);\n return;\n }\n if (brush.engine === 'smudge') {\n st.smudge!.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: d.opacity * d.alpha * brush.tip.opacity }, rect, cov);\n return;\n }\n const ex = st.exchange!;\n // 1. Pick up from under the tip (the pure footprint, as the Mixer Brush samples).\n ex.pickUp(this.medium.sample(rect, tip));\n // 2. The dab's paint: one colour, gradient paint's colour for this dab, or a colour per pixel across the tip.\n let flow = 1;\n if (this.sampler && !this.gradient) {\n const g = this.sampler.dab(d, this.sample);\n ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);\n flow = g.alpha;\n }\n let field: Float32Array | null = null;\n let total: number;\n let laid = cov;\n if (this.gradient) {\n total = this.tipField(ex, d, rect);\n field = this.field;\n if (this.gradientAlpha) laid = this.tipAlpha(d, rect, cov);\n } else total = ex.paint(this.paint);\n if (total > 0 && flow > 0) {\n if (this.coverage) this.depositWithCeiling(d, rect, laid, field, ex, flow);\n else {\n const dep = Math.min(1, total) * d.alpha * brush.tip.opacity * d.opacity * flow;\n if (dep > 0) this.medium.deposit(rect, laid, dep, this.paint, ex.paintPigment, field);\n }\n }\n // 3. Drain.\n ex.drain();\n }\n\n /** Fills `this.field` with the tip gradient (mixed with the carried paint) per pixel; returns the paint weight. */\n private tipField(ex: PaintExchange, d: PenDab, rect: RasterRect): number {\n const g = this.gradient!;\n const lut = this.fieldLut;\n let total = 0;\n for (let j = 0; j < g.length; j++) {\n ex.setReservoirColor(g[j][0], g[j][1], g[j][2]);\n const out: RgbTriple = lut[j] ?? [0, 0, 0];\n total = ex.paint(out);\n lut[j] = out;\n }\n const n = rect.width * rect.height;\n if (this.field.length < n * 3) this.field = new Float32Array(n * 3);\n const f = this.field;\n // Across the stroke: the normal to the direction of travel (document y points down).\n const nx = Math.sin(d.direction);\n const ny = Math.cos(d.direction);\n const r = Math.max(0.5, d.radius);\n const last = g.length - 1;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const py = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < rect.width; xx++, k++) {\n const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;\n const j = Math.min(last, Math.max(0, Math.round(((u + 1) / 2) * last)));\n f[k * 3] = lut[j][0];\n f[k * 3 + 1] = lut[j][1];\n f[k * 3 + 2] = lut[j][2];\n }\n }\n return total;\n }\n\n /** `cov` scaled by the gradient's opacity across the tip. */\n private tipAlpha(d: PenDab, rect: RasterRect, cov: Float32Array): Float32Array {\n const a = this.gradientAlpha!;\n const n = rect.width * rect.height;\n if (this.alphaCov.length < n) this.alphaCov = new Float32Array(n);\n const out = this.alphaCov.subarray(0, n);\n const nx = Math.sin(d.direction);\n const ny = Math.cos(d.direction);\n const r = Math.max(0.5, d.radius);\n const last = a.length - 1;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const py = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < rect.width; xx++, k++) {\n const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;\n out[k] = cov[k] * a[Math.min(last, Math.max(0, Math.round(((u + 1) / 2) * last)))];\n }\n }\n return out;\n }\n\n /** A dab brush's deposit: only what this dab adds to the stroke's coverage, under its opacity ceiling. */\n private depositWithCeiling(d: PenDab, rect: RasterRect, cov: Float32Array, field: Float32Array | null, ex: PaintExchange, flow = 1): void {\n const map = this.coverage!;\n const n = rect.width * rect.height;\n const before = new Float32Array(n);\n const vals = new Float32Array(n);\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, k++) {\n if (cov[k] <= 0) continue;\n before[k] = map.get(rect.x + xx, rect.y + yy);\n vals[k] = cov[k] * d.alpha * flow;\n }\n }\n map.accumulate(rect, vals, d.opacity);\n const o = this.brush.tip.opacity;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, k++) {\n if (cov[k] <= 0) {\n vals[k] = 0;\n continue;\n }\n const c0 = before[k] * o;\n const c1 = map.get(rect.x + xx, rect.y + yy) * o;\n vals[k] = c1 > c0 && c0 < 1 ? (c1 - c0) / (1 - c0) : 0;\n }\n }\n this.medium.deposit(rect, vals, 1, this.paint, ex.paintPigment, field);\n }\n}\n","/**\n * Thick paint (impasto): a pixel layer can carry two extra channels, a **height** map (how far the\n * paint stands up, 0..255) and a **material** map (R gloss, G metallic, 0..255), and it is lit at\n * render time: a normal from the height's slope, diffuse light from `PaintLighting`, and a\n * Blinn-Phong highlight whose sharpness follows the gloss, tinted by the paint where it is\n * metallic. The layer's own pixels stay the paint's colour (albedo), so relighting never repaints\n * anything, saving keeps colour, height and material apart, and only tiles whose inputs changed are\n * relit (the lit tiles are cached by the identity of their input tiles).\n *\n * Impasto strokes (`ThickStrokeDriver`) lay colour through the shared pick-up / mix exchange (paint\n * mixes into the wet paint it meets, like oil) and raise the height: `brush` with bristle ridges\n * along the stroke, `tube` in domed blobs, `knife` scraping the relief flat while it smears the\n * paint, `roller` as a flat stippled band, `gloop` as a thick glossy round bead. The paint runs out\n * along the stroke with `load` below 1, and `thinner` flattens and thins it.\n */\n\nimport { dabRect, rasterizeDab } from '../paint/brush';\nimport { SmudgeTip } from '../paint/blending';\nimport { exchangeRates, PaintExchange, TileMedium, type MixerLoad, type PaintTileTarget, type RgbTriple } from '../paint/exchange';\nimport { SRGB_TO_LINEAR, linearToSrgb8 } from '../paint/wet/film';\nimport { PaintGradientSampler, type PaintGradientSample, type PaintGradientSource } from '../paint/gradient-paint';\nimport { TILE, TiledSurface, finishTile, readRegion, surfaceFromPixels, tileKey, type RasterRect, type Tile, type TileKey } from '../raster/surface';\nimport { decodePng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport type { PaintBrush } from './brushes';\nimport { IDENTITY_COPY, PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine';\nimport { hash2 } from './noise';\n\n/** Where the light comes from. */\nexport interface PaintLighting {\n /** Degrees clockwise from the top of the canvas (315: top left, the default). */\n azimuth: number;\n /** Degrees above the canvas, 5 (grazing, deep shadows) .. 90 (straight on, no relief). Default 40. */\n elevation: number;\n /** How strongly the relief shows, 0 (flat) .. 1 (default 0.7). */\n relief: number;\n}\n\nexport const DEFAULT_LIGHTING: Readonly<PaintLighting> = { azimuth: 315, elevation: 40, relief: 0.7 };\n\nexport function normalizeLighting(raw: Partial<PaintLighting> = {}, base: PaintLighting = DEFAULT_LIGHTING): PaintLighting {\n const num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n return {\n azimuth: ((num(raw.azimuth, base.azimuth) % 360) + 360) % 360,\n elevation: Math.min(90, Math.max(5, num(raw.elevation, base.elevation))),\n relief: Math.min(1, Math.max(0, num(raw.relief, base.relief))),\n };\n}\n\n/** A layer's thick-paint channels (layer space, like its pixels). */\nexport interface ThickChannels {\n /** Height 0..255 per pixel (1 channel). */\n height: TiledSurface;\n /** Material per pixel (4 channels): R gloss, G metallic, B and A unused. */\n material: TiledSurface;\n}\n\nexport function emptyThickChannels(): ThickChannels {\n return { height: new TiledSurface(1, new Map(), 0), material: new TiledSurface(4, new Map(), 0) };\n}\n\n/** Height in pixels of the full 0..255 range, for the slopes. */\nconst RELIEF_PX = 7;\nconst AMBIENT = 0.38;\n\n/** Light vector (towards the light) for a lighting. */\nfunction lightVector(l: PaintLighting): [number, number, number] {\n const az = (l.azimuth * Math.PI) / 180;\n const el = (l.elevation * Math.PI) / 180;\n return [Math.cos(el) * Math.sin(az), -Math.cos(el) * Math.cos(az), Math.sin(el)];\n}\n\nexport function lightingKey(l: PaintLighting): string {\n return `${l.azimuth.toFixed(2)}|${l.elevation.toFixed(2)}|${l.relief.toFixed(3)}`;\n}\n\n/**\n * One tile of a thick layer lit: `color` (RGBA, straight), the height and material of the tile, and\n * the height of its 8 neighbours for the slopes at the border (`heightAt(dx, dy)` returns the\n * neighbouring tile's data or null). Returns a new RGBA tile.\n */\nexport function lightTile(color: Uint8ClampedArray, height: Uint8ClampedArray | null, heightAround: (dx: number, dy: number) => Uint8ClampedArray | null, material: Uint8ClampedArray | null, light: PaintLighting): Uint8ClampedArray {\n const out = new Uint8ClampedArray(color);\n if (!height || light.relief <= 0) return out;\n const [lx, ly, lz] = lightVector(light);\n const hv = [lx, ly, lz + 1];\n const hl = Math.hypot(hv[0], hv[1], hv[2]);\n const hx = hv[0] / hl;\n const hy = hv[1] / hl;\n const hz = hv[2] / hl;\n const k = (RELIEF_PX / 255) * light.relief * 2;\n const flatDiffuse = AMBIENT + (1 - AMBIENT) * lz;\n const read = (x: number, y: number): number => {\n if (x >= 0 && y >= 0 && x < TILE && y < TILE) return height[y * TILE + x];\n const dx = x < 0 ? -1 : x >= TILE ? 1 : 0;\n const dy = y < 0 ? -1 : y >= TILE ? 1 : 0;\n const t = heightAround(dx, dy);\n if (!t) return 0;\n return t[(y - dy * TILE) * TILE + (x - dx * TILE)];\n };\n for (let y = 0; y < TILE; y++) {\n for (let x = 0; x < TILE; x++) {\n const p = y * TILE + x;\n const i = p * 4;\n if (color[i + 3] === 0) continue;\n const h = height[p];\n const gx = (read(x + 1, y) - read(x - 1, y)) * 0.5;\n const gy = (read(x, y + 1) - read(x, y - 1)) * 0.5;\n if (h === 0 && gx === 0 && gy === 0) continue;\n let nx = -gx * k;\n let ny = -gy * k;\n const nl = Math.hypot(nx, ny, 1);\n nx /= nl;\n ny /= nl;\n const nz = 1 / nl;\n const diffuse = Math.max(0, nx * lx + ny * ly + nz * lz);\n const shade = (AMBIENT + (1 - AMBIENT) * diffuse) / flatDiffuse;\n const gloss = material ? material[i] / 255 : 0.3;\n const metal = material ? material[i + 1] / 255 : 0;\n const shininess = 6 + 90 * gloss;\n const nh = Math.max(0, nx * hx + ny * hy + nz * hz);\n // Only slopes catch a highlight: subtract what a flat surface would show.\n const spec = Math.max(0, Math.pow(nh, shininess) - Math.pow(hz, shininess)) * (0.25 + 0.75 * gloss) * light.relief;\n for (let c = 0; c < 3; c++) {\n const a = SRGB_TO_LINEAR[color[i + c]];\n const tint = metal * a + (1 - metal);\n const v = a * shade * (1 - 0.35 * metal) + spec * tint * (1 + metal);\n out[i + c] = linearToSrgb8(v);\n }\n }\n }\n return out;\n}\n\n/** Cache of lit tiles for one layer, by the identity of their inputs. */\nexport class LitCache {\n private readonly tiles = new Map<TileKey, { inputs: unknown[]; tile: Tile }>();\n\n /** The layer's pixels lit (tiles without relief nearby are the colour tiles themselves). */\n lit(color: TiledSurface, ch: ThickChannels, light: PaintLighting): TiledSurface {\n if (!ch.height.tiles.size || light.relief <= 0) return color;\n const lk = lightingKey(light);\n const out = new Map(color.tiles);\n const keys = new Set<TileKey>();\n for (const key of color.tiles.keys()) keys.add(key);\n for (const [key, tile] of color.tiles) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const around = (dx: number, dy: number) => ch.height.tiles.get(tileKey(tx + dx, ty + dy))?.data ?? null;\n let any = false;\n for (let dy = -1; dy <= 1 && !any; dy++) for (let dx = -1; dx <= 1 && !any; dx++) if (around(dx, dy)) any = true;\n if (!any) continue;\n const h = ch.height.tiles.get(key) ?? null;\n const m = ch.material.tiles.get(key) ?? null;\n const inputs: unknown[] = [tile, lk, m];\n for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(ch.height.tiles.get(tileKey(tx + dx, ty + dy)) ?? null);\n const cached = this.tiles.get(key);\n if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {\n out.set(key, cached.tile);\n continue;\n }\n const hd = h ? expand(h, 1) : null;\n const md = m ? expand(m, 4) : null;\n const data = lightTile(expand(tile, 4), hd, (dx, dy) => {\n const t = ch.height.tiles.get(tileKey(tx + dx, ty + dy));\n return t ? expand(t, 1) : null;\n }, md, light);\n const lit: Tile = { data, uniform: null };\n this.tiles.set(key, { inputs, tile: lit });\n out.set(key, lit);\n }\n for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);\n return new TiledSurface(4, out, color.defaultValue);\n }\n}\n\n/** A tile's pixels as a full buffer (uniform tiles are stored compactly). */\nfunction expand(t: Tile, channels: 1 | 4): Uint8ClampedArray {\n if (t.data.length === TILE * TILE * channels) return t.data;\n const out = new Uint8ClampedArray(TILE * TILE * channels);\n const u = t.uniform ?? 0;\n if (channels === 1) out.fill(u & 255);\n else {\n const bytes = [(u >>> 24) & 255, (u >>> 16) & 255, (u >>> 8) & 255, u & 255];\n for (let i = 0; i < out.length; i += 4) out.set(bytes, i);\n }\n return out;\n}\n\nexport interface ThickStrokeInit {\n brush: PaintBrush;\n /** Where the colour goes (the session's live tiles). */\n target: PaintTileTarget;\n /** The layer's channels before the stroke. */\n channels: ThickChannels;\n /** Paint colour, sRGB 0..1. */\n color: RgbTriple;\n load: MixerLoad;\n seed: number;\n copies: readonly StrokeCopy[];\n /** Gradient paint (resolved): the colour varies along the impasto. */\n paint?: PaintGradientSource | null;\n}\n\ninterface CopyState {\n walker: PenDabWalker;\n exchange: PaintExchange;\n smudge: SmudgeTip | null;\n /** Height patch carried by the knife. */\n carried: Float32Array | null;\n amount: number;\n}\n\n/** One impasto stroke: colour through the exchange, height and material into live channel tiles. */\nexport class ThickStrokeDriver {\n dabCount = 0;\n private readonly smoother: PenSmoother;\n private readonly states: CopyState[];\n private readonly medium: TileMedium;\n private readonly height = new Map<TileKey, Uint8ClampedArray>();\n private readonly material = new Map<TileKey, Uint8ClampedArray>();\n private readonly paint: RgbTriple = [0, 0, 0];\n private tip = new Float32Array(0);\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n private version = 0;\n\n constructor(readonly init: ThickStrokeInit) {\n const b = init.brush;\n this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);\n const copies = init.copies.length ? init.copies : [IDENTITY_COPY];\n this.copies = copies;\n const rates = exchangeRates(b.behavior.pickup, b.behavior.mix, 1, b.behavior.mixing);\n this.states = copies.map((c, i) => ({\n walker: new PenDabWalker(b, (init.seed + i * 7919) | 0, c.linear),\n exchange: new PaintExchange(rates, init.load),\n smudge: b.impasto.tool === 'knife' ? new SmudgeTip({ strength: 0.85, length: 0.6 + 0.38 * b.impasto.drag, mixing: b.behavior.mixing }, b.tip.size * 1.2) : null,\n carried: null,\n amount: 1,\n }));\n this.medium = new TileMedium(init.target, {});\n // Across the tip makes no sense for a loaded impasto tip: the colour follows the dab instead.\n this.sampler = init.paint ? new PaintGradientSampler(init.paint.map.by === 'across-tip' ? { ...init.paint, map: { by: 'distance', length: 300, repeat: 'mirror' } } : init.paint, init.seed) : null;\n }\n\n private readonly copies: readonly StrokeCopy[];\n private readonly sampler: PaintGradientSampler | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n\n /** What copy 0 carries after the dabs so far. */\n get load(): MixerLoad {\n return this.states[0].exchange.load;\n }\n\n /** The channels with the stroke so far (new objects whenever they changed). */\n channels(): ThickChannels {\n const h = new Map(this.init.channels.height.tiles);\n const m = new Map(this.init.channels.material.tiles);\n for (const [k, d] of this.height) {\n const t = finishTile(d, 1, 0);\n if (t) h.set(k, t);\n else h.delete(k);\n }\n for (const [k, d] of this.material) {\n const t = finishTile(new Uint8ClampedArray(d), 4, 0);\n if (t) m.set(k, t);\n else m.delete(k);\n }\n void this.version;\n return { height: new TiledSurface(1, h, 0), material: new TiledSurface(4, m, 0) };\n }\n\n private heightTile(tx: number, ty: number, write: boolean): Uint8ClampedArray | null {\n const key = tileKey(tx, ty);\n let t = this.height.get(key);\n if (t) {\n // Live tiles are replaced on write so caches keyed by identity see the change.\n return t;\n }\n const base = this.init.channels.height.tiles.get(key);\n if (!write) return base ? expand(base, 1) : null;\n t = base ? new Uint8ClampedArray(expand(base, 1)) : new Uint8ClampedArray(TILE * TILE);\n this.height.set(key, t);\n return t;\n }\n\n private materialTile(tx: number, ty: number): Uint8ClampedArray {\n const key = tileKey(tx, ty);\n let t = this.material.get(key);\n if (!t) {\n const base = this.init.channels.material.tiles.get(key);\n t = base ? new Uint8ClampedArray(expand(base, 4)) : new Uint8ClampedArray(TILE * TILE * 4);\n this.material.set(key, t);\n }\n return t;\n }\n\n private getH(x: number, y: number): number {\n const t = this.heightTile(Math.floor(x / TILE), Math.floor(y / TILE), false);\n return t ? t[(y & 255) * TILE + (x & 255)] : 0;\n }\n\n private setH(x: number, y: number, v: number): void {\n const t = this.heightTile(Math.floor(x / TILE), Math.floor(y / TILE), true)!;\n t[(y & 255) * TILE + (x & 255)] = v;\n }\n\n push(points: readonly PenPoint[]): RasterRect[] {\n const rects: RasterRect[] = [];\n for (const raw of points) {\n this.lastRaw = raw;\n const p = this.smoother.push(raw);\n this.lastFed = p;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n this.states[i].walker.push(c.map(p), dabs);\n this.applyDabs(i, dabs, rects);\n });\n }\n return rects;\n }\n\n finish(): RasterRect[] {\n const rects: RasterRect[] = [];\n const raw = this.lastRaw;\n const fed = this.lastFed;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);\n this.states[i].walker.finish(dabs);\n this.applyDabs(i, dabs, rects);\n });\n return rects;\n }\n\n private applyDabs(copy: number, dabs: PenDab[], rects: RasterRect[]): void {\n for (const d of dabs) {\n const rect = dabRect(d, this.init.brush.tip.hardness);\n if (rect.width <= 0 || rect.height <= 0) continue;\n this.dab(copy, d, rect);\n rects.push(rect);\n this.dabCount++;\n }\n if (dabs.length) this.version++;\n }\n\n private dab(copy: number, d: PenDab, rect: RasterRect): void {\n const b = this.init.brush;\n const im = b.impasto;\n const st = this.states[copy];\n const n = rect.width * rect.height;\n if (this.tip.length < n) this.tip = new Float32Array(n);\n const tip = this.tip.subarray(0, n);\n rasterizeDab({ ...d, alpha: 1 }, b.tip.hardness, rect, tip);\n const sel = this.init.target.selection;\n if (sel) {\n for (let yy = 0, p = 0; yy < rect.height; yy++)\n for (let xx = 0; xx < rect.width; xx++, p++) {\n const mx = rect.x + xx;\n const my = rect.y + yy;\n if (tip[p] > 0) tip[p] *= mx >= 0 && my >= 0 && mx < sel.width && my < sel.height ? sel.data[my * sel.width + mx] / 255 : 0;\n }\n }\n const off = this.init.target.offset;\n const k = Math.min(1, d.alpha * d.opacity * b.tip.opacity) * st.amount;\n const thick = im.thickness * (1 - 0.8 * im.thinner);\n // The paint runs out along the stroke.\n st.amount *= 1 - 0.06 * (1 - im.load);\n // Across-the-stroke coordinate for bristle ridges and the roller's stipple.\n const nxs = Math.sin(d.direction);\n const nys = Math.cos(d.direction);\n const r = Math.max(0.5, d.radius);\n\n if (im.tool === 'knife') {\n // Scrape: the relief flattens towards its low points and is dragged along; the paint smears.\n st.smudge!.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: k }, rect, tip);\n this.knifeHeight(st, d, rect, tip, k, im.drag);\n this.paintMaterial(rect, tip, k * 0.6, im.gloss, im.metallic);\n return;\n }\n // Colour: the exchange mixes the brush paint with the wet paint it picks up (like oil).\n const ex = st.exchange;\n let flow = 1;\n if (this.sampler) {\n const g = this.sampler.dab(d, this.sample);\n ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);\n flow = g.alpha;\n }\n ex.pickUp(this.medium.sample(rect, tip));\n const total = ex.paint(this.paint);\n ex.drain();\n const dep = Math.min(1, total) * k * (1 - 0.5 * im.thinner) * flow;\n if (dep > 0) {\n const cov = new Float32Array(n);\n for (let p = 0; p < n; p++) cov[p] = tip[p] > 0 ? Math.min(1, tip[p] * (im.tool === 'roller' || im.tool === 'gloop' || im.tool === 'tube' ? 1.6 : 1.1)) : 0;\n this.medium.deposit(rect, cov, dep, this.paint, ex.paintPigment);\n }\n // Height.\n const seed = this.init.seed;\n for (let yy = 0, p = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, p++) {\n const c = tip[p];\n if (c <= 0) continue;\n const dx = rect.x + xx;\n const dy = rect.y + yy;\n const lx = dx - off.x;\n const ly = dy - off.y;\n const u = ((dx + 0.5 - d.x) * nxs + (dy + 0.5 - d.y) * nys) / r;\n const h0 = this.getH(lx, ly);\n let h = h0;\n const full = 255 * thick * k;\n switch (im.tool) {\n case 'brush': {\n // Bristle ridges along the stroke: stripes across the tip, with a little noise.\n const ridge = 0.6 + 0.4 * Math.sin(u * 9 + hash2(Math.round(u * 6), 3, seed) * 6.28) + 0.15 * (hash2(lx, ly, seed) - 0.5);\n const target = full * c * ridge;\n // Wet paint pushed by the bristles: towards the ridge profile, building where it overlaps.\n h = Math.max(h0 * (1 - 0.25 * c * im.drag), Math.min(255, h0 * 0.35 + target));\n break;\n }\n case 'tube': {\n const dome = Math.sqrt(Math.max(0, c));\n h = Math.max(h0, Math.min(255, full * dome * 1.1));\n break;\n }\n case 'gloop': {\n const dome = Math.sqrt(Math.max(0, 1 - Math.min(1, Math.abs(u)) ** 2));\n h = Math.max(h0, Math.min(255, full * dome * (c > 0.2 ? 1 : c * 5)));\n break;\n }\n case 'roller': {\n const stipple = 0.75 + 0.5 * hash2(lx >> 1, ly >> 1, seed);\n h = h0 + (Math.min(255, full * 0.6 * stipple) - h0) * Math.min(1, c * 1.5);\n break;\n }\n }\n this.setH(lx, ly, Math.round(Math.max(0, Math.min(255, h))));\n }\n }\n this.paintMaterial(rect, tip, k, im.gloss * (1 - 0.3 * im.thinner), im.metallic);\n }\n\n /** The knife: flattens the relief under it and carries some of it along (a patch, like a smudge). */\n private knifeHeight(st: CopyState, d: PenDab, rect: RasterRect, tip: Float32Array, k: number, drag: number): void {\n const off = this.init.target.offset;\n const w = rect.width;\n const n = w * rect.height;\n const cur = new Float32Array(n);\n for (let yy = 0, p = 0; yy < rect.height; yy++) for (let xx = 0; xx < w; xx++, p++) cur[p] = this.getH(rect.x + xx - off.x, rect.y + yy - off.y);\n const carried = st.carried && st.carried.length === n ? st.carried : null;\n const next = new Float32Array(n);\n for (let yy = 0, p = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++, p++) {\n const c = tip[p] * k;\n if (c <= 0) {\n next[p] = cur[p];\n continue;\n }\n // Scrape flat: the blade levels to a thin film; what it carries is laid down at the trailing edge.\n const flat = cur[p] * (1 - 0.85 * c);\n const laid = carried ? carried[p] * drag * c : 0;\n const v = Math.max(flat, Math.min(255, flat * 0.5 + laid));\n next[p] = v;\n this.setH(rect.x + xx - off.x, rect.y + yy - off.y, Math.round(v));\n }\n }\n // What the blade picked up: the paint it scraped off.\n const pick = new Float32Array(n);\n for (let p = 0; p < n; p++) pick[p] = Math.max(0, cur[p] - next[p]) + (carried ? carried[p] * (1 - drag) * 0.5 : 0);\n st.carried = pick;\n }\n\n private paintMaterial(rect: RasterRect, tip: Float32Array, k: number, gloss: number, metal: number): void {\n const off = this.init.target.offset;\n for (let yy = 0, p = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, p++) {\n const c = Math.min(1, tip[p] * k);\n if (c <= 0) continue;\n const lx = rect.x + xx - off.x;\n const ly = rect.y + yy - off.y;\n const t = this.materialTile(Math.floor(lx / TILE), Math.floor(ly / TILE));\n const i = ((ly & 255) * TILE + (lx & 255)) * 4;\n t[i] = Math.round(t[i] + (gloss * 255 - t[i]) * c);\n t[i + 1] = Math.round(t[i + 1] + (metal * 255 - t[i + 1]) * c);\n t[i + 3] = 255;\n }\n }\n }\n}\n\n/** Height statistics of a layer's relief (for tools). */\nexport function reliefInfo(ch: ThickChannels): { bounds: RasterRect | null; maxHeight: number; tiles: number } {\n let max = 0;\n for (const t of ch.height.tiles.values()) {\n if (t.uniform !== null) max = Math.max(max, t.uniform & 255);\n else for (let i = 0; i < t.data.length; i++) if (t.data[i] > max) max = t.data[i];\n }\n return { bounds: ch.height.bounds, maxHeight: max, tiles: ch.height.tiles.size };\n}\n\n/** A layer's thick-paint channels as saved: PNG data URLs over a rectangle (layer space). */\nexport interface SavedThickChannels {\n rect: RasterRect;\n /** Greyscale PNG of the height. */\n height: string;\n /** RGBA PNG of the material (R gloss, G metallic). */\n material: string;\n}\n\n/** Channels → saved form (`null` when there is no relief). */\nexport function thickToJSON(ch: ThickChannels): SavedThickChannels | null {\n const b = ch.height.bounds;\n if (!b) return null;\n return { rect: { ...b }, height: pngDataUrl(b.width, b.height, readRegion(ch.height, b), 1), material: pngDataUrl(b.width, b.height, readRegion(ch.material, b), 4) };\n}\n\n/** Saved form → channels; throws a sentence when it cannot be read. */\nexport function thickFromJSON(raw: unknown): ThickChannels {\n const r = raw as Partial<SavedThickChannels>;\n if (!r || typeof r !== 'object' || !r.rect || typeof r.height !== 'string' || typeof r.material !== 'string') throw new Error('not saved thick-paint channels');\n const decode = (url: string, gray: boolean) => {\n const parsed = parseDataUrl(url);\n if (!parsed) throw new Error('a channel is not a PNG data URL');\n return decodePng(parsed.bytes, { keepGray: gray });\n };\n const h = decode(r.height, true);\n const m = decode(r.material, false);\n const hd = h.channels === 1 ? h.data : Uint8ClampedArray.from({ length: h.width * h.height }, (_, i) => h.data[i * 4]);\n return {\n height: surfaceFromPixels({ width: h.width, height: h.height, data: hd }, 1, r.rect.x, r.rect.y),\n material: surfaceFromPixels({ width: m.width, height: m.height, data: m.data }, 4, r.rect.x, r.rect.y),\n };\n}\n","/**\n * A live stroke on one pixel layer. Points arrive while the pen moves; the layer shows the stroke\n * so far through a live surface (no history); `end()` commits it as one undo step.\n *\n * - Dab and eraser brushes: the coverage is cumulative, so each update recomposites the touched\n * region from the pre-stroke tile (`applyStrokeToTile` with `into` and `region`).\n * - Dab-by-dab brushes (`PaintDabStroke`: the Mixer Brush, blender, smudge, and dab brushes that\n * pick up paint or are multi-coloured): dabs read what earlier dabs laid down, so they paint\n * into live tile copies; every tile written since the last update gets a new `Tile` object,\n * because the compositor caches by tile identity.\n */\n\nimport { applyStrokeToTile, type StrokeApply } from '../paint/brush';\nimport { EMPTY_MIXER_LOAD, type PaintTileTarget } from '../paint/exchange';\nimport { cleanMixer, loadMixer, type MixerLoad } from '../paint/mixer';\nimport { TILE, TiledSurface, finishTile, tileKey, tilesInRect, type RasterRect, type Tile, type TileKey } from '../raster/surface';\nimport { usesPaintExchange, type PaintBrush } from './brushes';\nimport type { PaintGradientSource } from '../paint/gradient-paint';\nimport { PaintDabStroke } from './dab-stroke';\nimport { DryStroke, IDENTITY_COPY, type PenPoint, type StrokeCopy } from './engine';\nimport type { PaperGrain } from './paper';\nimport type { Affine } from './strokes';\nimport type { WetStrokeDriver } from './wet';\nimport { ThickStrokeDriver, type ThickChannels } from './thick';\nimport { BristleStroke, ParticleStroke, type CoverageStroke } from './procedural';\nimport { StickerSpray, type Sticker } from './stickers';\n\nexport interface StrokeSessionHost {\n /** Shows the layer's live surface (no history); `rects` are where it changed. */\n update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;\n /** Ends the stroke: `surface` is the new pixels, or `null` when nothing changed / it was cancelled. */\n commit(session: PaintStrokeSession, surface: TiledSurface | null, cancelled: boolean): void;\n}\n\nexport interface StrokeSessionInit {\n layerId: string;\n surface: TiledSurface;\n offset: { x: number; y: number };\n lockTransparency: boolean;\n brush: PaintBrush;\n /** Paint colour 0..255 RGBA. */\n color: readonly [number, number, number, number];\n /** What the eraser paints on a transparency-locked layer (the paper colour). */\n background: readonly [number, number, number, number];\n grain: PaperGrain | null;\n paperDepth: number;\n seed: number;\n /** The paint colour as #rrggbb (recorded with the stroke). */\n colorHex: string;\n /** The stroke's extra copies as affine maps (recorded with the stroke); `copies` holds all of them as engine copies. */\n affineCopies: readonly Affine[];\n copies: readonly StrokeCopy[];\n /** Applied to each raw sample before it is painted and recorded (perspective snapping). */\n inputFilter?: (p: PenPoint) => PenPoint;\n /** Mixer and wet-capable dab brushes: what the brush carries at the start of the stroke. */\n mixerLoad: MixerLoad;\n /** The secondary colour 0..255 RGBA (a multi-colour brush without its own colours uses it). */\n secondary?: readonly [number, number, number, number];\n /**\n * Wet brushes: the driver that applies the stroke to the wet layer, and the layer's current\n * pixels (the wet layer's display). The session's own surface is not used.\n */\n wet?: { driver: WetStrokeDriver; surface: () => TiledSurface };\n /** Impasto brushes: the layer's thick-paint channels before the stroke. */\n thick?: { channels: ThickChannels };\n /** Where paint may land (canvas-sized coverage 0..255: the stencils' mask), or none. */\n selection?: { width: number; height: number; data: ArrayLike<number> } | null;\n /** Sticker brushes: the next sticker number on the layer. */\n sticker?: { nextId: number };\n /** Gradient paint (resolved: concrete colours), or `null` for the one colour. Recorded with the stroke. */\n paint?: PaintGradientSource | null;\n}\n\nexport interface PaintStrokeResult {\n layerId: string;\n /** Document rectangle the stroke touched, or `null` when no pixel changed. */\n bounds: RasterRect | null;\n dabs: number;\n /** Id of the recorded stroke (see `list_strokes` / `replay_strokes`), or `null` when nothing changed. */\n strokeId: string | null;\n /** Mixer and wet-capable dab brushes: what the brush carries after the stroke (cleaned unless it is dirty). */\n mixerLoad?: MixerLoad;\n /** Wet brushes: the paint is wet (it keeps moving until it dries; see dry_paint / advance_wet_paint). */\n wet?: boolean;\n}\n\nfunction defaultTile(surface: TiledSurface): Uint8ClampedArray {\n const data = new Uint8ClampedArray(TILE * TILE * surface.channels);\n if (surface.defaultValue) data.fill(surface.defaultValue);\n return data;\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\nfunction union(a: RasterRect | null, b: RasterRect): RasterRect {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\nexport class PaintStrokeSession {\n readonly layerId: string;\n /** The raw input so far (what a replay feeds back in). */\n readonly points: PenPoint[] = [];\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private readonly fresh = new Set<TileKey>();\n private readonly dry: CoverageStroke | null = null;\n private readonly wet: PaintDabStroke | null = null;\n private readonly wetDriver: WetStrokeDriver | null = null;\n private readonly thickDriver: ThickStrokeDriver | null = null;\n private readonly stickerSpray: StickerSpray | null = null;\n /** A wet stroke that ended while the simulation was mid-batch: it commits when the driver catches up. */\n private wetEndPending = false;\n private readonly apply: StrokeApply;\n private readonly copies: readonly StrokeCopy[];\n private bounds: RasterRect | null = null;\n private lastRaw: PenPoint | null = null;\n private closed = false;\n result: PaintStrokeResult | null = null;\n /** The mask stencils this stroke was painted with when they are not the editor's (replays). */\n maskStencils: readonly import('./stencils').Stencil[] | null = null;\n\n constructor(private readonly host: StrokeSessionHost, readonly init: StrokeSessionInit) {\n this.layerId = init.layerId;\n this.base = init.surface;\n this.live = new Map(this.base.tiles);\n this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];\n const brush = init.brush;\n if (init.wet) this.wetDriver = init.wet.driver;\n this.apply = {\n mode: brush.engine === 'eraser' ? 'erase' : 'paint',\n color: init.color,\n background: init.background,\n opacity: brush.tip.opacity,\n blend: brush.tip.blend,\n lockTransparency: init.lockTransparency,\n selection: init.selection ?? null,\n offset: init.offset,\n };\n if (this.wetDriver) {\n // the wet layer paints; nothing else to set up\n } else if (init.sticker) {\n this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null });\n } else if (init.thick) {\n const rgb = (c: readonly number[]): [number, number, number] => [c[0] / 255, c[1] / 255, c[2] / 255];\n this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null });\n } else if (brush.engine === 'mixer' || brush.engine === 'blender' || brush.engine === 'smudge' || usesPaintExchange(brush, Boolean(init.paint))) {\n const rgb = (c: readonly number[]): [number, number, number] => [c[0] / 255, c[1] / 255, c[2] / 255];\n this.wet = new PaintDabStroke(brush, {\n target: this.tileTarget(),\n color: rgb(init.color),\n secondary: rgb(init.secondary ?? init.background),\n load: init.mixerLoad,\n grain: init.grain,\n paperDepth: init.paperDepth,\n seed: init.seed,\n copies: this.copies,\n paint: init.paint ?? null,\n });\n } else {\n const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null };\n this.dry = brush.engine === 'particle' ? new ParticleStroke(brush, opts) : brush.engine === 'bristle' ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);\n }\n }\n\n private tileTarget(): PaintTileTarget {\n return {\n offset: this.init.offset,\n lockTransparency: this.init.lockTransparency,\n selection: this.init.selection ?? null,\n readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? 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 open(): boolean {\n return !this.closed;\n }\n\n /** Adds raw pen samples (document pixels). */\n push(points: readonly PenPoint[]): void {\n if (this.closed || !points.length) return;\n const filter = this.init.inputFilter;\n if (filter) points = points.map(filter);\n for (const p of points) this.points.push(p);\n this.lastRaw = points[points.length - 1];\n if (this.wetDriver) {\n this.wetUpdate(this.wetDriver.push(points));\n return;\n }\n this.update(this.dry ? this.dry.push(points) : this.thickDriver ? this.thickDriver.push(points) : this.stickerSpray ? this.stickerSpray.push(points) : this.wet!.push(points));\n }\n\n /** Sticker strokes: the stickers sprayed so far. */\n stickers(): Sticker[] | null {\n return this.stickerSpray ? this.stickerSpray.stickers.map((s) => ({ ...s })) : null;\n }\n\n /** Impasto strokes: the layer's thick-paint channels with the stroke so far. */\n thickChannels(): ThickChannels | null {\n return this.thickDriver?.channels() ?? null;\n }\n\n /** Shows a wet stroke's progress (the wet layer's display). */\n wetUpdate(rects: RasterRect[]): void {\n if (!rects.length || !this.init.wet) return;\n for (const r of rects) this.bounds = union(this.bounds, r);\n this.host.update(this.layerId, this.init.wet.surface(), rects);\n }\n\n /** Whether a wet stroke has ended but waits for the simulation to finish a batch. */\n get pendingEnd(): boolean {\n return this.wetEndPending;\n }\n\n /** Completes a wet stroke whose end waited (the controller calls it when the driver is done). */\n completeWetEnd(): void {\n if (!this.wetEndPending || !this.wetDriver || !this.wetDriver.done) return;\n this.wetEndPending = false;\n this.finishWet();\n }\n\n private finishWet(): void {\n const d = this.wetDriver!;\n const changed = d.dabCount > 0;\n this.result = { layerId: this.layerId, bounds: changed ? d.bounds : null, dabs: d.dabCount, strokeId: null, wet: true };\n const carried = d.load;\n if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);\n this.host.commit(this, changed ? this.init.wet!.surface() : null, false);\n }\n\n private update(rects: RasterRect[]): void {\n if (!rects.length) return;\n for (const r of rects) this.bounds = union(this.bounds, r);\n const ch = this.base.channels;\n if (!this.dry) {\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.host.update(this.layerId, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);\n return;\n }\n const off = this.init.offset;\n const regions = new Map<TileKey, { tx: number; ty: number; r: RasterRect }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - off.x, y: r.y - off.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 ? union(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(this.dry.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.host.update(this.layerId, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Ends the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new Error('This stroke has already ended.');\n if (this.wetDriver) {\n this.wetUpdate(this.wetDriver.finish());\n this.closed = true;\n if (this.wetDriver.done) this.finishWet();\n else {\n this.wetEndPending = true;\n this.result = { layerId: this.layerId, bounds: this.bounds, dabs: this.wetDriver.dabCount, strokeId: null, wet: true };\n }\n return this.result!;\n }\n this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet!.finish());\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 tile = finishTile(this.live.get(key)!.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 // An impasto stroke can change only the relief (a knife on dry colour).\n if (this.thickDriver && this.thickDriver.dabCount > 0) changed = true;\n const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;\n const dabs = this.dry ? this.dry.dabCount : this.thickDriver ? this.thickDriver.dabCount : this.stickerSpray ? this.stickerSpray.dabCount : this.wet!.dabCount;\n this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };\n const carried = this.wet?.load ?? this.thickDriver?.load;\n if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);\n this.host.commit(this, surface, false);\n return this.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.host.commit(this, null, true);\n }\n}\n\n/** The Mixer Brush load a stroke starts with. */\nexport function mixerLoadForStroke(brush: PaintBrush, current: MixerLoad, color: readonly [number, number, number]): MixerLoad {\n if (!brush.loadsColor) return { reservoir: null, amount: 0, pickup: brush.mixer.cleanEachStroke ? null : current.pickup };\n if (brush.mixer.loadEachStroke || !current.reservoir) return loadMixer(current, color);\n return current;\n}\n\n/**\n * What any brush carries at the start of a stroke: the Mixer Brush as `mixerLoadForStroke`; a\n * wet-capable dab brush the paint colour, full, plus the carried paint when it is dirty; other\n * brushes nothing.\n */\nexport function brushLoadForStroke(brush: PaintBrush, current: MixerLoad, color: readonly [number, number, number]): MixerLoad {\n if (brush.engine === 'mixer') return mixerLoadForStroke(brush, current, color);\n if (brush.engine === 'impasto') return { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null };\n if (brush.engine === 'wet') return brush.wet.tool === 'paint' ? { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null } : EMPTY_MIXER_LOAD;\n if (usesPaintExchange(brush)) return { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null };\n return EMPTY_MIXER_LOAD;\n}\n","/**\n * Stencils: shapes laid on the canvas that guide or mask strokes, like a ruler, a French curve or a\n * masking template held over the paper.\n *\n * - `ruler` (a straight edge) and `arc` (a circle or part of one) **guide**: a stroke that starts\n * within `snap` pixels of the edge follows it exactly; one that starts further away is free.\n * - `line` (a straight edge across the canvas) and `shape` (rectangle, ellipse or polygon)\n * **mask**: paint lands only on one side of the line, or only inside the shape (outside with\n * `invert`), with an anti-aliased edge. Several masks combine (paint lands where all allow it).\n * - Any stencil can be switched to the other `mode`: a shape can guide (strokes follow its\n * outline) and a ruler can mask (paint on one side of it).\n *\n * Geometry only; `stencilMask` makes the canvas coverage the stroke engines read as a selection.\n */\n\nimport type { Point2 } from './guides';\n\nexport type StencilKind = 'ruler' | 'arc' | 'line' | 'shape';\nexport const STENCIL_KINDS: readonly StencilKind[] = ['ruler', 'arc', 'line', 'shape'];\n\nexport type StencilShape = 'rect' | 'ellipse' | 'polygon';\nexport const STENCIL_SHAPES: readonly StencilShape[] = ['rect', 'ellipse', 'polygon'];\n\nexport interface Stencil {\n id: string;\n kind: StencilKind;\n /** `guide`: strokes that start near the edge follow it; `mask`: paint lands only inside (or on one side). */\n mode: 'guide' | 'mask';\n /** Shown and in effect (default true). */\n enabled: boolean;\n /** ruler / line: two points on the edge (document pixels). */\n from?: Point2;\n to?: Point2;\n /** arc: centre and radius, and the part of the circle in degrees clockwise from +x (default the whole circle). */\n center?: Point2;\n radius?: number;\n startAngle?: number;\n endAngle?: number;\n /** shape: its kind and geometry (`rect` for rect and ellipse, `points` for a polygon). */\n shape?: StencilShape;\n rect?: { x: number; y: number; width: number; height: number };\n points?: Point2[];\n /** mask: paint outside the shape (or on the other side of the line) instead. */\n invert: boolean;\n /** guide: how near the edge (px) a stroke must start to follow it (default 40). */\n snap: number;\n /** mask: soft edge width in pixels (default 1, anti-aliased). */\n feather: number;\n}\n\nexport type StencilInput = Partial<Omit<Stencil, 'id'>> & { kind: StencilKind };\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst pt = (p: unknown): Point2 | undefined => (p && finite((p as Point2).x) && finite((p as Point2).y) ? { x: (p as Point2).x, y: (p as Point2).y } : undefined);\n\n/** A stencil with every field in range; throws a sentence naming what is missing. */\nexport function normalizeStencil(raw: StencilInput, id: string, base?: Stencil): Stencil {\n const kind = STENCIL_KINDS.includes(raw.kind) ? raw.kind : base?.kind;\n if (!kind) throw new Error(`kind ${JSON.stringify(raw.kind)} is not a stencil; use ${STENCIL_KINDS.join(', ')}.`);\n const s: Stencil = {\n id,\n kind,\n mode: raw.mode === 'guide' || raw.mode === 'mask' ? raw.mode : (base?.mode ?? (kind === 'ruler' || kind === 'arc' ? 'guide' : 'mask')),\n enabled: typeof raw.enabled === 'boolean' ? raw.enabled : (base?.enabled ?? true),\n invert: typeof raw.invert === 'boolean' ? raw.invert : (base?.invert ?? false),\n snap: finite(raw.snap) ? Math.min(400, Math.max(1, raw.snap)) : (base?.snap ?? 40),\n feather: finite(raw.feather) ? Math.min(100, Math.max(0, raw.feather)) : (base?.feather ?? 1),\n };\n if (kind === 'ruler' || kind === 'line') {\n s.from = pt(raw.from) ?? base?.from;\n s.to = pt(raw.to) ?? base?.to;\n if (!s.from || !s.to) throw new Error(`A ${kind} needs from and to points ({x, y} in document pixels).`);\n if (Math.hypot(s.to.x - s.from.x, s.to.y - s.from.y) < 1) throw new Error(`A ${kind}'s from and to must be at least 1 px apart.`);\n } else if (kind === 'arc') {\n s.center = pt(raw.center) ?? base?.center;\n s.radius = finite(raw.radius) ? raw.radius : base?.radius;\n if (!s.center || !(s.radius! > 0)) throw new Error('An arc needs a center ({x, y}) and a radius above 0.');\n s.startAngle = finite(raw.startAngle) ? raw.startAngle : (base?.startAngle ?? 0);\n s.endAngle = finite(raw.endAngle) ? raw.endAngle : (base?.endAngle ?? 360);\n } else {\n s.shape = STENCIL_SHAPES.includes(raw.shape as StencilShape) ? raw.shape : (base?.shape ?? 'rect');\n if (s.shape === 'polygon') {\n const pts = Array.isArray(raw.points) ? raw.points.map(pt).filter((p): p is Point2 => Boolean(p)) : base?.points;\n if (!pts || pts.length < 3) throw new Error('A polygon stencil needs at least 3 points.');\n s.points = pts;\n } else {\n const r = raw.rect ?? base?.rect;\n if (!r || !finite(r.x) || !finite(r.y) || !(r.width > 0) || !(r.height > 0)) throw new Error(`A ${s.shape} stencil needs rect {x, y, width, height} with a width and height above 0.`);\n s.rect = { x: r.x, y: r.y, width: r.width, height: r.height };\n }\n }\n return s;\n}\n\n/** The outline of a stencil as a polyline (document pixels), for drawing and for shape guides. */\nexport function stencilOutline(s: Stencil): Point2[] {\n if (s.kind === 'ruler' || s.kind === 'line') return [s.from!, s.to!];\n if (s.kind === 'arc') {\n const a0 = (s.startAngle! * Math.PI) / 180;\n const a1 = (s.endAngle! * Math.PI) / 180;\n const n = Math.max(8, Math.ceil(Math.abs(a1 - a0) * Math.max(8, s.radius! / 4)));\n return Array.from({ length: n + 1 }, (_, i) => {\n const a = a0 + ((a1 - a0) * i) / n;\n return { x: s.center!.x + Math.cos(a) * s.radius!, y: s.center!.y + Math.sin(a) * s.radius! };\n });\n }\n if (s.shape === 'polygon') return [...s.points!, s.points![0]];\n const r = s.rect!;\n if (s.shape === 'rect') return [{ x: r.x, y: r.y }, { x: r.x + r.width, y: r.y }, { x: r.x + r.width, y: r.y + r.height }, { x: r.x, y: r.y + r.height }, { x: r.x, y: r.y }];\n const n = 96;\n return Array.from({ length: n + 1 }, (_, i) => {\n const a = (i / n) * Math.PI * 2;\n return { x: r.x + r.width / 2 + (Math.cos(a) * r.width) / 2, y: r.y + r.height / 2 + (Math.sin(a) * r.height) / 2 };\n });\n}\n\n/** The nearest point of a polyline to `p`, and its distance. */\nfunction nearestOnPolyline(line: readonly Point2[], p: Point2): { point: Point2; distance: number } {\n let best = { point: line[0], distance: Infinity };\n for (let i = 0; i + 1 < line.length; i++) {\n const a = line[i];\n const b = line[i + 1];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len2 = dx * dx + dy * dy || 1;\n const t = Math.min(1, Math.max(0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));\n const q = { x: a.x + dx * t, y: a.y + dy * t };\n const d = Math.hypot(p.x - q.x, p.y - q.y);\n if (d < best.distance) best = { point: q, distance: d };\n }\n return best;\n}\n\n/** The nearest point on a stencil's edge (a ruler extends without end; an arc is exact). */\nfunction nearestOnStencil(s: Stencil, p: Point2): { point: Point2; distance: number } {\n if (s.kind === 'ruler' || s.kind === 'line') {\n const a = s.from!;\n const dx = s.to!.x - a.x;\n const dy = s.to!.y - a.y;\n const len2 = dx * dx + dy * dy;\n const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;\n const q = { x: a.x + dx * t, y: a.y + dy * t };\n return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };\n }\n if (s.kind === 'arc') {\n const c = s.center!;\n const ang = Math.atan2(p.y - c.y, p.x - c.x);\n const full = Math.abs(s.endAngle! - s.startAngle!) >= 360;\n if (full) {\n const q = { x: c.x + Math.cos(ang) * s.radius!, y: c.y + Math.sin(ang) * s.radius! };\n return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };\n }\n return nearestOnPolyline(stencilOutline(s), p);\n }\n return nearestOnPolyline(stencilOutline(s), p);\n}\n\n/**\n * Snaps a stroke to the guide stencils: the first point picks the nearest guide within its `snap`\n * distance (or none), and every point after it is moved onto that guide's edge.\n */\nexport class StencilSnapper {\n private guide: Stencil | null | undefined;\n\n constructor(private readonly stencils: readonly Stencil[]) {}\n\n /** The stencil the stroke follows, once chosen (`null`: none). */\n get following(): Stencil | null {\n return this.guide ?? null;\n }\n\n snap<P extends Point2>(p: P): P {\n if (this.guide === undefined) {\n let best: { s: Stencil; d: number } | null = null;\n for (const s of this.stencils) {\n if (!s.enabled || s.mode !== 'guide') continue;\n const { distance } = nearestOnStencil(s, p);\n if (distance <= s.snap && (!best || distance < best.d)) best = { s, d: distance };\n }\n this.guide = best?.s ?? null;\n }\n if (!this.guide) return p;\n const q = nearestOnStencil(this.guide, p).point;\n return { ...p, x: q.x, y: q.y };\n }\n}\n\n/** Signed distance to a mask stencil's edge: positive where paint may land. */\nfunction maskDistance(s: Stencil, x: number, y: number): number {\n let d: number;\n if (s.kind === 'ruler' || s.kind === 'line') {\n // The left side of from → to (screen coordinates, y down) is \"inside\".\n const dx = s.to!.x - s.from!.x;\n const dy = s.to!.y - s.from!.y;\n const len = Math.hypot(dx, dy);\n d = ((x - s.from!.x) * dy - (y - s.from!.y) * dx) / len;\n } else if (s.kind === 'arc') {\n d = s.radius! - Math.hypot(x - s.center!.x, y - s.center!.y);\n } else if (s.shape === 'rect') {\n const r = s.rect!;\n d = Math.min(x - r.x, r.x + r.width - x, y - r.y, r.y + r.height - y);\n } else if (s.shape === 'ellipse') {\n const r = s.rect!;\n const rx = r.width / 2;\n const ry = r.height / 2;\n const nx = (x - r.x - rx) / rx;\n const ny = (y - r.y - ry) / ry;\n // Approximate distance: the radial distance scaled by the local radius.\n const k = Math.hypot(nx, ny);\n d = (1 - k) * Math.min(rx, ry);\n } else {\n const pts = s.points!;\n let inside = false;\n for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {\n const a = pts[i];\n const b = pts[j];\n if (a.y > y !== b.y > y && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x) inside = !inside;\n }\n const e = nearestOnPolyline([...pts, pts[0]], { x, y }).distance;\n d = inside ? e : -e;\n }\n return s.invert ? -d : d;\n}\n\n/**\n * The combined coverage of the enabled mask stencils over a `width × height` canvas (0..255 per\n * pixel), or `null` when no stencil masks.\n */\nexport function stencilMask(stencils: readonly Stencil[], width: number, height: number): { width: number; height: number; data: Uint8Array } | null {\n const masks = stencils.filter((s) => s.enabled && s.mode === 'mask');\n if (!masks.length) return null;\n const data = new Uint8Array(width * height);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n let cov = 1;\n for (const s of masks) {\n const f = Math.max(0.5, s.feather);\n const d = maskDistance(s, x + 0.5, y + 0.5);\n cov = Math.min(cov, Math.min(1, Math.max(0, d / (2 * f) + 0.5)));\n if (cov <= 0) break;\n }\n data[y * width + x] = Math.round(cov * 255);\n }\n }\n return { width, height, data };\n}\n\n/** A stencil scaled about the origin by `k` (for repainting at a larger size). */\nexport function scaleStencil(s: Stencil, k: number): Stencil {\n const p = (q?: Point2) => (q ? { x: q.x * k, y: q.y * k } : undefined);\n return {\n ...s,\n ...(s.from ? { from: p(s.from) } : {}),\n ...(s.to ? { to: p(s.to) } : {}),\n ...(s.center ? { center: p(s.center), radius: s.radius! * k } : {}),\n ...(s.rect ? { rect: { x: s.rect.x * k, y: s.rect.y * k, width: s.rect.width * k, height: s.rect.height * k } } : {}),\n ...(s.points ? { points: s.points.map((q) => p(q)!) } : {}),\n snap: s.snap * k,\n feather: s.feather * k,\n };\n}\n","/**\n * Strokes as data. Every committed stroke is recorded with the brush, colour, raw pen samples,\n * jitter seed and symmetry copies it was painted with, so it replays to the same pixels, and can\n * be replayed moved, scaled, rotated, recoloured or with another brush (`replay_strokes`).\n */\n\nimport { normalizePaintBrush, type PaintBrushInput, type PaintBrush } from './brushes';\nimport type { Linear2, PenPoint, StrokeCopy } from './engine';\nimport type { MixerLoad } from '../paint/exchange';\nimport { normalizeStencil, type Stencil } from './stencils';\nimport { readPaintSource, scalePaintGradient, type PaintGradientSource } from '../paint/gradient-paint';\n\n/** An affine map [a, b, c, d, e, f]: (x, y) → (a·x + b·y + e, c·x + d·y + f). */\nexport type Affine = readonly [number, number, number, number, number, number];\n\nexport const IDENTITY_AFFINE: Affine = [1, 0, 0, 1, 0, 0];\n\nexport interface RecordedStroke {\n id: string;\n layerId: string;\n brush: PaintBrush;\n /** #rrggbb */\n color: string;\n /** Raw pen samples in document pixels, before smoothing. */\n points: PenPoint[];\n seed: number;\n /** Extra copies painted with the stroke (mirror / kaleidoscope), as affine maps of the points. */\n copies?: Affine[];\n /** What the brush carried at the start (Mixer Brush and wet-capable brushes), so a replay mixes the same. */\n load?: MixerLoad;\n /** Wet brushes: the wet simulation's clock (steps) at which each point was applied, so a replay lets the paint flow the same. */\n steps?: number[];\n /** The mask stencils it was painted through (so a replay keeps to them). */\n stencils?: Stencil[];\n /** Gradient paint, with concrete colours (`color` is then its first stop's colour, for older readers). */\n paint?: PaintGradientSource;\n}\n\nexport function applyAffine(m: Affine, x: number, y: number): { x: number; y: number } {\n return { x: m[0] * x + m[1] * y + m[4], y: m[2] * x + m[3] * y + m[5] };\n}\n\n/** `a ∘ b` (b first). */\nexport function composeAffine(a: Affine, b: Affine): Affine {\n return [\n a[0] * b[0] + a[1] * b[2],\n a[0] * b[1] + a[1] * b[3],\n a[2] * b[0] + a[3] * b[2],\n a[2] * b[1] + a[3] * b[3],\n a[0] * b[4] + a[1] * b[5] + a[4],\n a[2] * b[4] + a[3] * b[5] + a[5],\n ];\n}\n\nexport function invertAffine(m: Affine): Affine {\n const det = m[0] * m[3] - m[1] * m[2];\n if (Math.abs(det) < 1e-12) throw new Error('The transform is not invertible (scale 0?).');\n const a = m[3] / det;\n const b = -m[1] / det;\n const c = -m[2] / det;\n const d = m[0] / det;\n return [a, b, c, d, -(a * m[4] + b * m[5]), -(c * m[4] + d * m[5])];\n}\n\n/** The pen sample mapped by `m` (tilt and twist are kept: they describe the pen, not the path). */\nexport function mapPenPoint(m: Affine, p: PenPoint): PenPoint {\n const q = applyAffine(m, p.x, p.y);\n return { ...p, x: q.x, y: q.y };\n}\n\n/** An affine copy as the engine's `StrokeCopy`. */\nexport function affineCopy(m: Affine): StrokeCopy {\n const linear: Linear2 = [m[0], m[1], m[2], m[3]];\n return { map: (p) => mapPenPoint(m, p), linear };\n}\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\n\nfunction readPoint(raw: unknown): PenPoint | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n if (!finite(r.x) || !finite(r.y)) return null;\n const p: PenPoint = { x: r.x, y: r.y };\n for (const k of ['pressure', 't', 'tiltX', 'tiltY', 'altitude', 'azimuth', 'twist'] as const) if (finite(r[k])) p[k] = r[k] as number;\n return p;\n}\n\nfunction readAffine(raw: unknown): Affine | null {\n return Array.isArray(raw) && raw.length === 6 && raw.every(finite) ? (raw as unknown as Affine) : null;\n}\n\nfunction readRgb(raw: unknown): [number, number, number] | null {\n return Array.isArray(raw) && raw.length === 3 && raw.every(finite) ? (raw.map((v) => Math.min(1, Math.max(0, v))) as [number, number, number]) : null;\n}\n\nfunction readLoad(raw: unknown): MixerLoad | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const reservoir = readRgb(r.reservoir);\n const pickup = readRgb(r.pickup);\n if (!reservoir && !pickup) return null;\n return { reservoir, amount: reservoir && finite(r.amount) ? Math.min(1, Math.max(0, r.amount)) : 0, pickup };\n}\n\n/** A recorded stroke read from JSON, or `null` when it is not one. */\nexport function normalizeBrushStroke(raw: unknown): RecordedStroke | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n if (typeof r.id !== 'string' || typeof r.layerId !== 'string' || !Array.isArray(r.points)) return null;\n const points = r.points.map(readPoint).filter((p): p is PenPoint => p !== null);\n if (!points.length) return null;\n const out: RecordedStroke = {\n id: r.id,\n layerId: r.layerId,\n brush: normalizePaintBrush((r.brush as PaintBrushInput) ?? {}),\n color: typeof r.color === 'string' && /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : '#000000',\n points,\n seed: finite(r.seed) ? r.seed | 0 : 0,\n };\n const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m): m is Affine => m !== null) : [];\n if (copies.length) out.copies = copies;\n const load = readLoad(r.load);\n if (load) out.load = load;\n if (Array.isArray(r.stencils)) {\n const st: Stencil[] = [];\n for (const raw of r.stencils as Stencil[]) {\n try {\n st.push(normalizeStencil(raw, typeof raw?.id === 'string' ? raw.id : 'stencil'));\n } catch {\n // an unreadable stencil is dropped\n }\n }\n if (st.length) out.stencils = st;\n }\n const paint = readPaintSource(r.paint);\n if (paint && paint.type === 'gradient') out.paint = paint;\n if (Array.isArray(r.steps) && r.steps.length === points.length && r.steps.every((v) => Number.isInteger(v) && v >= 0)) out.steps = [...(r.steps as number[])];\n return out;\n}\n\n/** The bounding box of a stroke's points. */\nexport function strokeBounds(points: readonly PenPoint[]): { x: number; y: number; width: number; height: number } {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const p of points) {\n if (p.x < x0) x0 = p.x;\n if (p.y < y0) y0 = p.y;\n if (p.x > x1) x1 = p.x;\n if (p.y > y1) y1 = p.y;\n }\n return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };\n}\n\nexport interface StrokeVariation {\n /** Move by this many document pixels (default 0). */\n dx?: number;\n dy?: number;\n /** Scale about the origin (default 1); brush size scales with it. */\n scale?: number;\n /** Rotate about the origin, degrees clockwise (default 0). */\n rotate?: number;\n /** Mirror horizontally about the origin (default false). */\n flipX?: boolean;\n /** The origin for scale, rotate and flip (default: the centre of the strokes' bounds). */\n origin?: { x: number; y: number };\n /** Multiply every pressure by this (default 1). */\n pressureScale?: number;\n}\n\n/** The affine map of a variation about `origin`. */\nexport function variationAffine(v: StrokeVariation, origin: { x: number; y: number }): Affine {\n const s = v.scale ?? 1;\n const a = ((v.rotate ?? 0) * Math.PI) / 180;\n const cos = Math.cos(a) * s;\n const sin = Math.sin(a) * s;\n const fx = v.flipX ? -1 : 1;\n // Translate to the origin, flip, rotate (clockwise with y down) and scale, translate back, move.\n const linear: Affine = [cos * fx, -sin, sin * fx, cos, 0, 0];\n const toOrigin: Affine = [1, 0, 0, 1, -origin.x, -origin.y];\n const back: Affine = [1, 0, 0, 1, origin.x + (v.dx ?? 0), origin.y + (v.dy ?? 0)];\n return composeAffine(back, composeAffine(linear, toOrigin));\n}\n\n/** A stroke varied by `m` (points mapped; symmetry copies conjugated so they stay symmetric). */\nexport function varyStroke(stroke: RecordedStroke, m: Affine, v: StrokeVariation): RecordedStroke {\n const s = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));\n const ps = v.pressureScale ?? 1;\n const points = stroke.points.map((p) => {\n const q = mapPenPoint(m, p);\n if (ps !== 1) q.pressure = Math.min(1, Math.max(0, (p.pressure ?? 1) * ps));\n return q;\n });\n const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;\n const out: RecordedStroke = { ...stroke, brush, points };\n if (stroke.paint && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);\n if (stroke.copies?.length) {\n const inv = invertAffine(m);\n out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));\n }\n return out;\n}\n","/**\n * Compositing for the paint editor, on the shared `RasterCompositor`: the paper is a content layer\n * at the bottom of the stack (its tiles are generated from the paper settings), and view-only\n * layers (the tracing image) sit between it and the paint. No compositor of our own.\n */\n\nimport { RasterCompositor, type CompositorLayer, type CompositorProfile, type RasterSources } from '../raster/compositor';\nimport type { RasterRect } from '../raster/surface';\nimport type { PaintLayer } from './document';\nimport { PaperGrain, renderPaper, type PaperSettings } from './paper';\n\n/** The paper as a layer of the compositor (generated, never stored). */\nexport interface PaperNode extends CompositorLayer {\n type: 'paper';\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: 'normal';\n paper: PaperSettings;\n /** The light, for metallic paper. */\n light?: { azimuth: number; elevation: number };\n}\n\n/** A view-only RGBA image under the paint (the tracing image), drawn at its opacity. */\nexport interface ImageNode extends CompositorLayer {\n type: 'image';\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: 'normal';\n /** Canvas-sized RGBA. */\n image: { width: number; height: number; data: Uint8ClampedArray };\n /** Changes when the pixels change. */\n version: number;\n}\n\nexport type ViewLayer = PaintLayer | PaperNode | ImageNode;\n\nexport const PAPER_NODE_ID = '__paper';\nexport const TRACING_NODE_ID = '__tracing';\n\nexport function paperKey(p: PaperSettings, light?: { azimuth: number; elevation: number }): string {\n const metal = p.metallic ?? 0;\n return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry, metal, metal > 0 && light ? [light.azimuth, light.elevation] : null]);\n}\n\nconst paperNodes = new WeakMap<PaperSettings, PaperNode>();\n\n/** The paper as a compositor layer (one object per paper settings object and light, so caches hold). */\nexport function paperNode(paper: PaperSettings, light?: { azimuth: number; elevation: number }): PaperNode {\n let n = paperNodes.get(paper);\n const metal = (paper.metallic ?? 0) > 0;\n if (!n || (metal && (n.light?.azimuth !== light?.azimuth || n.light?.elevation !== light?.elevation))) {\n n = { type: 'paper', id: PAPER_NODE_ID, name: 'Paper', visible: true, opacity: 1, blend: 'normal', paper, ...(metal && light ? { light: { azimuth: light.azimuth, elevation: light.elevation } } : {}) };\n paperNodes.set(paper, n);\n }\n return n;\n}\n\nconst grainCache = new Map<string, PaperGrain>();\nconst tileCache = new Map<string, Uint8ClampedArray>();\n\n/** The paper's dry height field, built once per settings. */\nexport function paperGrain(paper: PaperSettings): PaperGrain {\n const key = JSON.stringify([paper.pattern, paper.seed, paper.scale, paper.tiling, paper.dry]);\n let g = grainCache.get(key);\n if (!g) {\n g = new PaperGrain(paper, 'dry');\n if (grainCache.size > 16) grainCache.delete(grainCache.keys().next().value as string);\n grainCache.set(key, g);\n }\n return g;\n}\n\nfunction paperTile(paper: PaperSettings, r: RasterRect, light?: { azimuth: number; elevation: number }): Uint8ClampedArray {\n const key = `${paperKey(paper, light)}|${r.x},${r.y},${r.width},${r.height}`;\n let t = tileCache.get(key);\n if (!t) {\n t = renderPaper(paper, r, paperGrain(paper), light);\n if (tileCache.size > 512) tileCache.delete(tileCache.keys().next().value as string);\n tileCache.set(key, t);\n }\n return t;\n}\n\nconst PROFILE: CompositorProfile<ViewLayer, RasterSources> = {\n contentTile(layer, r) {\n if (layer.type === 'paper') return paperTile((layer as PaperNode).paper, r, (layer as PaperNode).light);\n if (layer.type === 'image') {\n const { image } = layer as ImageNode;\n const out = new Uint8ClampedArray(r.width * r.height * 4);\n const x0 = Math.max(0, r.x);\n const y0 = Math.max(0, r.y);\n const x1 = Math.min(image.width, r.x + r.width);\n const y1 = Math.min(image.height, r.y + r.height);\n if (x1 <= x0 || y1 <= y0) return null;\n for (let y = y0; y < y1; y++) out.set(image.data.subarray((y * image.width + x0) * 4, (y * image.width + x1) * 4), ((y - r.y) * r.width + (x0 - r.x)) * 4);\n return out;\n }\n return null;\n },\n layerKey(layer) {\n if (layer.type === 'paper') return paperKey((layer as PaperNode).paper, (layer as PaperNode).light);\n if (layer.type === 'image') return (layer as ImageNode).version;\n return undefined;\n },\n};\n\n/** A compositor for paint editor stacks (paper and view-only images included). */\nexport function createPaintCompositor(): RasterCompositor<ViewLayer, RasterSources> {\n return new RasterCompositor<ViewLayer, RasterSources>(PROFILE);\n}\n\n/**\n * Area-averaged downscale of straight-alpha RGBA (colours weighted by alpha). Sizes at or above\n * the source copy it.\n */\nexport function downsampleRgba(data: ArrayLike<number>, width: number, height: number, tw: number, th: number): Uint8ClampedArray {\n tw = Math.max(1, Math.round(tw));\n th = Math.max(1, Math.round(th));\n if (tw >= width && th >= height) return Uint8ClampedArray.from(data as ArrayLike<number>);\n const out = new Uint8ClampedArray(tw * th * 4);\n const sx = width / tw;\n const sy = height / th;\n for (let y = 0; y < th; y++) {\n const y0 = y * sy;\n const y1 = y0 + sy;\n for (let x = 0; x < tw; x++) {\n const x0 = x * sx;\n const x1 = x0 + sx;\n let r = 0;\n let g = 0;\n let b = 0;\n let a = 0;\n let w = 0;\n for (let yy = Math.floor(y0); yy < Math.ceil(y1) && yy < height; yy++) {\n const wy = Math.min(y1, yy + 1) - Math.max(y0, yy);\n for (let xx = Math.floor(x0); xx < Math.ceil(x1) && xx < width; xx++) {\n const wgt = wy * (Math.min(x1, xx + 1) - Math.max(x0, xx));\n const i = (yy * width + xx) * 4;\n const al = data[i + 3] * wgt;\n r += data[i] * al;\n g += data[i + 1] * al;\n b += data[i + 2] * al;\n a += al;\n w += wgt;\n }\n }\n const o = (y * tw + x) * 4;\n if (a > 0) {\n out[o] = r / a;\n out[o + 1] = g / a;\n out[o + 2] = b / a;\n }\n out[o + 3] = w > 0 ? a / w : 0;\n }\n }\n return out;\n}\n","/**\n * Saved paint documents: JSON plus PNG assets. Layers go through the image editor's layer\n * serializer (`serializeLayers` / `deserializeLayers`), so pixel layers and groups are stored the\n * same way in both editors, including external asset stores. The paper is plain settings: it is\n * regenerated on load, never stored as pixels.\n *\n * Forward compatibility: readers keep fields they do not know at the top level (`extensions`) and\n * write them back, so a later version can add per-layer wet state, height or material channels\n * as new assets keyed by layer id without breaking older readers (see `PaintDocumentExtensions`).\n */\n\nimport {\n deserializeLayers,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayer,\n} from '../image-editor/layers/serialize';\nimport type { ImageLayer } from '../image-editor/layers/model';\nimport { findLayer, flattenLayers } from '../raster/layers';\nimport { normalizeBrushStroke, type RecordedStroke } from './strokes';\nimport { normalizeCanvas, type PaintDocumentState, type PaintEvent, type PaintLayer } from './document';\nimport { normalizePaper, type PaperSettings } from './paper';\nimport type { WetEvent } from './wet';\n\nexport const PAINT_FORMAT = 'pixerate-paint' as const;\nexport const PAINT_SCHEMA_VERSION = 1 as const;\n\n/** Where a host keeps layer PNGs outside the JSON (the image editor's asset store contract). */\nexport type PaintAssetStore = ImageEditorAssetStore;\n\n/**\n * Fields a later version adds. Phase 1 writes none of them and keeps any it reads. Planned:\n * `layerChannels: { [layerId]: { height?: asset, material?: asset } }` for impasto and\n * metallic paint. Wet paint itself is never saved (saving dries it); its settings are (`wet`).\n */\nexport type PaintDocumentExtensions = Record<string, unknown>;\n\nexport interface SerializedPaintDocument {\n format: typeof PAINT_FORMAT;\n version: typeof PAINT_SCHEMA_VERSION;\n width: number;\n height: number;\n dpi: number;\n paper: PaperSettings;\n /** Bottom to top: pixel layers and groups, as the image editor stores them. */\n layers: SerializedLayer[];\n /** Asset id → PNG data URL (or an external reference). */\n assets: Record<string, string>;\n activeLayerId: string | null;\n /** Recorded strokes (strokes are data), oldest first; missing when saved without them. */\n strokes?: RecordedStroke[];\n /** Everything else (document-level state of later phases), written back as read. */\n [extension: string]: unknown;\n}\n\nconst KNOWN = new Set(['format', 'version', 'width', 'height', 'dpi', 'paper', 'layers', 'assets', 'activeLayerId', 'strokes', 'guides', 'swatches', 'mixingPad', 'wet', 'wetEvents', 'paintEvents']);\n\n/** Fields of a saved document this version does not read, kept for writing back. */\nexport function documentExtensions(raw: Record<string, unknown>): PaintDocumentExtensions {\n const out: PaintDocumentExtensions = {};\n for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;\n return out;\n}\n\nexport interface SerializeOptions {\n /** Include the recorded strokes (default true). */\n strokes?: boolean;\n /** Later-phase fields to write as they were read. */\n extensions?: PaintDocumentExtensions;\n /** Painting guides (symmetry, perspective, tracing and reference images as PNG data URLs). */\n guides?: unknown;\n swatches?: unknown;\n /** The mixing pad: `{ current?: png data URL, saved: [{ id, name, png }] }`. */\n mixingPad?: unknown;\n /** Wet media settings: `{ tilt, layers: { [layerId]: { params, flow } } }`. */\n wet?: unknown;\n}\n\nfunction header(state: PaintDocumentState, options: SerializeOptions): Omit<SerializedPaintDocument, 'layers' | 'assets'> {\n const out: Omit<SerializedPaintDocument, 'layers' | 'assets'> = {\n ...(options.extensions ?? {}),\n format: PAINT_FORMAT,\n version: PAINT_SCHEMA_VERSION,\n width: state.canvas.width,\n height: state.canvas.height,\n dpi: state.canvas.dpi,\n paper: { ...state.paper, dry: { ...state.paper.dry }, wet: { ...state.paper.wet } },\n activeLayerId: state.activeLayerId,\n };\n if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));\n if (options.guides !== undefined) out.guides = JSON.parse(JSON.stringify(options.guides));\n if (options.swatches !== undefined) out.swatches = JSON.parse(JSON.stringify(options.swatches));\n if (options.mixingPad !== undefined) (out as Record<string, unknown>).mixingPad = JSON.parse(JSON.stringify(options.mixingPad));\n if (options.wet !== undefined) (out as Record<string, unknown>).wet = JSON.parse(JSON.stringify(options.wet));\n if (state.wetEvents?.length) (out as Record<string, unknown>).wetEvents = JSON.parse(JSON.stringify(state.wetEvents));\n if (state.paintEvents?.length) (out as Record<string, unknown>).paintEvents = JSON.parse(JSON.stringify(state.paintEvents));\n return out;\n}\n\n/** The document as JSON with inline PNG data URLs. */\nexport function serializePaintDocument(state: PaintDocumentState, options: SerializeOptions = {}): SerializedPaintDocument {\n const { layers, assets } = serializeLayers(state.layers as unknown as ImageLayer[]);\n return { ...header(state, options), layers, assets } as SerializedPaintDocument;\n}\n\n/** The document as JSON with each PNG uploaded to `store` once (by content). */\nexport async function serializePaintDocumentExternal(state: PaintDocumentState, store: PaintAssetStore, options: SerializeOptions = {}): Promise<SerializedPaintDocument> {\n const { layers, assets } = await serializeLayersExternal(state.layers as unknown as ImageLayer[], store);\n return { ...header(state, options), layers, assets } as SerializedPaintDocument;\n}\n\nexport interface LoadedPaintDocument {\n state: PaintDocumentState;\n extensions: PaintDocumentExtensions;\n /** The saved guides and swatches, as stored (the controller validates them). */\n guides?: unknown;\n swatches?: unknown;\n /** The saved mixing pad, as stored (the controller validates it). */\n mixingPad?: unknown;\n /** The saved wet settings, as stored (the controller validates them). */\n wet?: unknown;\n /** What could not be read (the rest still loads). */\n problems: string[];\n}\n\n/** Keeps pixel layers and groups (the paint editor's kinds); reports and drops the rest. */\nfunction paintLayersOnly(layers: ImageLayer[], problems: string[]): PaintLayer[] {\n const out: PaintLayer[] = [];\n for (const l of layers) {\n if (l.type === 'pixel') {\n const { filters, source, ...rest } = l;\n if (filters?.length) problems.push(`Layer \"${l.name}\": its filters are an image editor feature and were dropped.`);\n if (source) problems.push(`Layer \"${l.name}\" refers to an image editor source image; it loads empty.`);\n out.push(rest as PaintLayer);\n } else if (l.type === 'group') out.push({ ...l, children: paintLayersOnly(l.children, problems) } as PaintLayer);\n else problems.push(`Layer \"${l.name}\" is an image editor ${l.type} layer, which the paint editor does not have; it was left out.`);\n }\n return out;\n}\n\nfunction readStrokes(raw: unknown, problems: string[]): RecordedStroke[] {\n if (!Array.isArray(raw)) return [];\n const out: RecordedStroke[] = [];\n raw.forEach((s, i) => {\n const stroke = normalizeBrushStroke(s);\n if (stroke) out.push(stroke);\n else problems.push(`strokes[${i}] is not a recorded stroke; skipped.`);\n });\n return out;\n}\n\n/** Reads a saved document with inline assets. Throws when it is not a paint document at all. */\nexport function deserializePaintDocument(raw: unknown): LoadedPaintDocument {\n if (!raw || typeof raw !== 'object') throw new Error('Not a paint document: expected a JSON object.');\n const r = raw as Record<string, unknown>;\n if (r.format !== PAINT_FORMAT) throw new Error(`Not a paint document: format is ${JSON.stringify(r.format)}, expected \"${PAINT_FORMAT}\".`);\n if (typeof r.version !== 'number' || r.version > PAINT_SCHEMA_VERSION) {\n throw new Error(`Paint document version ${JSON.stringify(r.version)} is newer than this editor reads (${PAINT_SCHEMA_VERSION}); update @pixerate/editor.`);\n }\n const problems: string[] = [];\n const canvas = normalizeCanvas({ width: r.width as number, height: r.height as number, dpi: (r.dpi as number | undefined) ?? undefined });\n const paper = normalizePaper((r.paper as Record<string, unknown>) ?? {});\n const assets = r.assets && typeof r.assets === 'object' ? (r.assets as Record<string, string>) : {};\n const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);\n const wanted = typeof r.activeLayerId === 'string' ? r.activeLayerId : null;\n const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : (flattenLayers(layers).filter((l) => l.type === 'pixel').pop()?.id ?? null);\n const wetEvents = Array.isArray(r.wetEvents) ? (r.wetEvents as WetEvent[]).filter((e) => e && typeof e === 'object' && Number.isInteger(e.clock) && typeof e.kind === 'string') : [];\n const state: PaintDocumentState = { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) };\n if (wetEvents.length) state.wetEvents = wetEvents;\n const paintEvents = Array.isArray(r.paintEvents) ? (r.paintEvents as PaintEvent[]).filter((e) => e && typeof e === 'object' && Number.isInteger(e.at) && typeof e.layerId === 'string' && ['fill', 'clear', 'flatten', 'stickers'].includes(e.kind)) : [];\n if (paintEvents.length) state.paintEvents = paintEvents;\n return { state, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, mixingPad: r.mixingPad, wet: r.wet, problems };\n}\n\n/** Reads a saved document whose assets may be external references in `store`. */\nexport async function deserializePaintDocumentAsync(raw: unknown, store?: PaintAssetStore): Promise<LoadedPaintDocument> {\n if (!raw || typeof raw !== 'object') throw new Error('Not a paint document: expected a JSON object.');\n const r = raw as Record<string, unknown>;\n const problems: string[] = [];\n const assets = await resolveExternalAssets((r.assets as Record<string, string>) ?? {}, store, problems);\n const loaded = deserializePaintDocument({ ...r, assets });\n return { ...loaded, problems: [...problems, ...loaded.problems] };\n}\n","/**\n * The canvas view: zoom, pan, rotation and mirroring. View state only: the pixels never resample,\n * the view maps document pixels to the screen and pointer positions back.\n */\n\nimport type { Affine } from './strokes';\nimport { applyAffine, invertAffine } from './strokes';\n\nexport interface PaintView {\n /** Screen pixels per document pixel (0.02..64, default 1). */\n zoom: number;\n /** Offset of the canvas centre from the viewport centre, in screen pixels. */\n pan: { x: number; y: number };\n /** Rotation in degrees clockwise, -180..180 (default 0). */\n rotation: number;\n /** Mirror the view left to right (default false). */\n mirror: boolean;\n}\n\nexport const MIN_ZOOM = 0.02;\nexport const MAX_ZOOM = 64;\n\nexport const DEFAULT_VIEW: Readonly<PaintView> = { zoom: 1, pan: { x: 0, y: 0 }, rotation: 0, mirror: false };\n\n/** Degrees into -180..180. */\nexport function normalizeRotation(deg: number): number {\n if (!Number.isFinite(deg)) return 0;\n let r = deg % 360;\n if (r > 180) r -= 360;\n if (r <= -180) r += 360;\n return Math.round(r * 1000) / 1000;\n}\n\nexport function normalizeView(raw: Partial<PaintView>, base: PaintView = DEFAULT_VIEW): PaintView {\n const zoom = typeof raw.zoom === 'number' && Number.isFinite(raw.zoom) ? Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, raw.zoom)) : base.zoom;\n const pan = raw.pan && Number.isFinite(raw.pan.x) && Number.isFinite(raw.pan.y) ? { x: raw.pan.x, y: raw.pan.y } : { ...base.pan };\n return {\n zoom,\n pan,\n rotation: typeof raw.rotation === 'number' ? normalizeRotation(raw.rotation) : base.rotation,\n mirror: typeof raw.mirror === 'boolean' ? raw.mirror : base.mirror,\n };\n}\n\n/**\n * Document → screen for a canvas of `canvas` size shown in a viewport of `viewport` size:\n * the canvas centre goes to the viewport centre plus `pan`, then zoom, rotation and mirroring\n * apply about it.\n */\nexport function viewMatrix(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }): Affine {\n const a = (view.rotation * Math.PI) / 180;\n const z = view.zoom;\n const m = view.mirror ? -1 : 1;\n const cos = Math.cos(a) * z;\n const sin = Math.sin(a) * z;\n // Screen = C + R(θ) · M · z · (p − c), with y down (clockwise rotation).\n const cx = canvas.width / 2;\n const cy = canvas.height / 2;\n const sx = viewport.width / 2 + view.pan.x;\n const sy = viewport.height / 2 + view.pan.y;\n const A = cos * m;\n const B = -sin;\n const C = sin * m;\n const D = cos;\n return [A, B, C, D, sx - (A * cx + B * cy), sy - (C * cx + D * cy)];\n}\n\n/** Screen position → document position. */\nexport function screenToDocument(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }, x: number, y: number): { x: number; y: number } {\n return applyAffine(invertAffine(viewMatrix(view, canvas, viewport)), x, y);\n}\n\n/** Document position → screen position. */\nexport function documentToScreen(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }, x: number, y: number): { x: number; y: number } {\n return applyAffine(viewMatrix(view, canvas, viewport), x, y);\n}\n\n/** A CSS `matrix(...)` for an element of the canvas size placed at the viewport's top-left. */\nexport function viewCssTransform(view: PaintView, canvas: { width: number; height: number }, viewport: { width: number; height: number }): string {\n const m = viewMatrix(view, canvas, viewport);\n const f = (v: number) => Math.round(v * 10000) / 10000;\n return `matrix(${f(m[0])}, ${f(m[2])}, ${f(m[1])}, ${f(m[3])}, ${f(m[4])}, ${f(m[5])})`;\n}\n\n/** The zoom that fits the canvas in the viewport (with `margin` screen pixels around it). */\nexport function fitZoom(canvas: { width: number; height: number }, viewport: { width: number; height: number }, margin = 24): number {\n const w = Math.max(1, viewport.width - 2 * margin);\n const h = Math.max(1, viewport.height - 2 * margin);\n return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min(w / canvas.width, h / canvas.height)));\n}\n\n/**\n * Pen rotation as the document sees it: a rotated or mirrored view turns the pen's azimuth and\n * barrel rotation too, so a tilted pencil shades the same way on screen however the canvas is turned.\n */\nexport function viewPenToDocument<P extends { azimuth?: number; twist?: number }>(view: PaintView, p: P): P {\n if (!view.rotation && !view.mirror) return p;\n const out = { ...p };\n const r = (view.rotation * Math.PI) / 180;\n if (typeof p.azimuth === 'number') {\n let a = p.azimuth - r;\n if (view.mirror) a = Math.PI - a;\n out.azimuth = ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);\n }\n if (typeof p.twist === 'number') {\n let t = p.twist - view.rotation;\n if (view.mirror) t = -t;\n out.twist = ((t % 360) + 360) % 360;\n }\n return out;\n}\n","/**\n * Pen input helpers for the bindings: Pointer Events (structurally typed, no DOM import) into\n * `PenPoint`s, palm rejection by hand, and which hand the user draws with. The bindings feed\n * coalesced events into the stroke and draw predicted ones as a throwaway preview.\n */\n\nimport { penOrientation, type PenPoint } from './engine';\nimport { viewPenToDocument, type PaintView } from './view';\nimport { pointerPressure } from '../paint/pointer';\n\n/** The Pointer Event fields the paint editor reads. */\nexport interface PointerSample {\n pointerType?: string;\n pressure?: number;\n tiltX?: number;\n tiltY?: number;\n /** Radians (Pointer Events Level 3). */\n altitudeAngle?: number;\n azimuthAngle?: number;\n /** Degrees 0..359. */\n twist?: number;\n timeStamp?: number;\n}\n\n/** Which hand holds the pen: palm rejection ignores touches on that side of it. */\nexport type Hand = 'right' | 'left';\n\n/**\n * Pressure of a pointer sample: pens report it; mice report 0.5 while pressed and pens 0 on\n * `pointerup`, both of which mean \"use full\" (see GOTCHAS). The shared `pointerPressure`.\n */\nexport function samplePressure(e: PointerSample): number {\n return pointerPressure(e);\n}\n\n/**\n * A pointer sample at document position (x, y) as a pen point. Only pens report tilt and twist;\n * a pen's lean and barrel rotation are turned with the view, so marks look the same on screen\n * whatever the canvas rotation and mirroring.\n */\nexport function pointerToPenPoint(e: PointerSample, x: number, y: number, view?: PaintView): PenPoint {\n const p: PenPoint = { x, y, pressure: samplePressure(e) };\n if (typeof e.timeStamp === 'number' && Number.isFinite(e.timeStamp)) p.t = e.timeStamp;\n if (e.pointerType !== 'pen') return p;\n const o = penOrientation({\n x,\n y,\n altitude: typeof e.altitudeAngle === 'number' ? e.altitudeAngle : undefined,\n azimuth: typeof e.azimuthAngle === 'number' ? e.azimuthAngle : undefined,\n tiltX: e.tiltX,\n tiltY: e.tiltY,\n });\n // A pen standing straight up with no tilt reported keeps no orientation (most pens without tilt report 0, 0).\n if (o && o.altitude < Math.PI / 2 - 1e-6) {\n p.altitude = o.altitude;\n p.azimuth = o.azimuth;\n }\n if (typeof e.twist === 'number' && Number.isFinite(e.twist) && e.twist !== 0) p.twist = e.twist;\n return view ? viewPenToDocument(view, p) : p;\n}\n\n/**\n * Palm rejection: whether a touch at `touch` (screen pixels) is probably the drawing hand resting\n * on the screen while the pen is at `pen`. A right hand rests below and to the right of the pen, a\n * left hand below and to the left.\n */\nexport function isPalmTouch(touch: { x: number; y: number }, pen: { x: number; y: number } | null, hand: Hand): boolean {\n if (!pen) return false;\n const dx = touch.x - pen.x;\n const dy = touch.y - pen.y;\n const side = hand === 'right' ? dx : -dx;\n return dy > -40 && side > -40 && Math.hypot(dx, dy) < 600;\n}\n","/**\n * JPEG and WebP encoding. PNG is encoded in pure TypeScript (`encodePng`); the lossy formats need\n * the platform's codecs, so they go through an `OffscreenCanvas` (browsers and Workers) or a host\n * encoder. Nothing is imported: the globals are looked up at call time.\n */\n\nexport type ImageFormat = 'image/png' | 'image/jpeg' | 'image/webp';\n\n/** Encodes straight-alpha RGBA as `format` at `quality` (0..1). */\nexport type ImageEncoder = (image: { width: number; height: number; data: Uint8ClampedArray }, format: 'image/jpeg' | 'image/webp', quality: number) => Promise<Uint8Array>;\n\ninterface CanvasLike {\n getContext(type: '2d'): { putImageData(data: unknown, x: number, y: number): void } | null;\n convertToBlob(options: { type: string; quality: number }): Promise<{ arrayBuffer(): Promise<ArrayBuffer>; type: string }>;\n}\n\n/** An encoder through `OffscreenCanvas`, or `null` where there is none (Node, tests). */\nexport function defaultImageEncoder(): ImageEncoder | null {\n const g = globalThis as unknown as {\n OffscreenCanvas?: new (w: number, h: number) => CanvasLike;\n ImageData?: new (data: Uint8ClampedArray, w: number, h: number) => unknown;\n };\n if (typeof g.OffscreenCanvas !== 'function' || typeof g.ImageData !== 'function') return null;\n const Canvas = g.OffscreenCanvas;\n const Data = g.ImageData;\n return async (image, format, quality) => {\n const canvas = new Canvas(image.width, image.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('No 2D context to encode with.');\n ctx.putImageData(new Data(new Uint8ClampedArray(image.data), image.width, image.height), 0, 0);\n const blob = await canvas.convertToBlob({ type: format, quality });\n if (blob.type !== format) throw new Error(`This browser cannot encode ${format} (it produced ${blob.type || 'nothing'}); export PNG instead.`);\n return new Uint8Array(await blob.arrayBuffer());\n };\n}\n\n/**\n * Decodes an image file (PNG, JPEG, WebP, … whatever the browser reads) into RGBA pixels through\n * `createImageBitmap` and an `OffscreenCanvas`. Browser and Worker only.\n */\nexport async function decodeImageBlob(blob: Blob): Promise<{ width: number; height: number; data: Uint8ClampedArray }> {\n const g = globalThis as unknown as {\n createImageBitmap?: (b: Blob) => Promise<{ width: number; height: number; close?(): void }>;\n OffscreenCanvas?: new (w: number, h: number) => { getContext(type: '2d'): { drawImage(img: unknown, x: number, y: number): void; getImageData(x: number, y: number, w: number, h: number): { data: Uint8ClampedArray } } | null };\n };\n if (typeof g.createImageBitmap !== 'function' || typeof g.OffscreenCanvas !== 'function') throw new Error('Decoding images needs a browser (createImageBitmap and OffscreenCanvas).');\n const bitmap = await g.createImageBitmap(blob);\n const canvas = new g.OffscreenCanvas(bitmap.width, bitmap.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('No 2D context to decode with.');\n ctx.drawImage(bitmap, 0, 0);\n const data = new Uint8ClampedArray(ctx.getImageData(0, 0, bitmap.width, bitmap.height).data);\n bitmap.close?.();\n return { width: bitmap.width, height: bitmap.height, data };\n}\n\n/** Saves bytes as a file through a temporary link. Browser only. */\nexport function downloadBytes(bytes: Uint8Array | string, fileName: string, mimeType: string): void {\n const g = globalThis as unknown as {\n document?: { createElement(tag: 'a'): { href: string; download: string; click(): void } };\n URL?: { createObjectURL(b: Blob): string; revokeObjectURL(u: string): void };\n Blob?: new (parts: unknown[], options: { type: string }) => Blob;\n };\n if (!g.document || !g.URL || !g.Blob) throw new Error('Downloading needs a browser.');\n const url = g.URL.createObjectURL(new g.Blob([bytes], { type: mimeType }));\n const a = g.document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => g.URL!.revokeObjectURL(url), 1000);\n}\n","/**\n * Wet media in the paint editor (watercolour first): a layer painted with a wet brush gets a wet\n * layer in the document's `WetWorld` (`@pixerate/editor/paint`), and its pixels are the world's\n * display of it until the paint dries.\n *\n * - **Wet brushes** (`engine: 'wet'`) run through `WetStrokeDriver`: the same pen input as dry\n * media (`PenDabWalker`: pressure, tilt, rotation, jitter, symmetry copies), each dab's tip\n * coverage (quantised to 1/4096, so tiny floating-point differences between engines never reach\n * the simulation) applied by the brush's wet tool. `paint` goes through the shared pick-up / mix\n * exchange over a `WetMedium`, so a wet brush lifts and mixes wet paint like the phase 2 brushes.\n * - **Time** is the world's step clock. The host calls `tick` every frame (the bindings do); it\n * steps at `speed` steps per second unless paused. Agents call `advance` or `dry`.\n * - **Dabs between steps.** Edits only happen between batches: points pushed while a batch runs\n * (in Web Workers) wait for it, and every raw point records the clock it was applied at\n * (`RecordedStroke.steps`), so a stroke replays exactly.\n * - **Drying.** Tiles that have dried are baked into the layer's pixels by the world; when a whole\n * layer is dry its wet layer is dropped and it is an ordinary pixel layer again. `dry` dries at\n * once. Saving and exporting use the deterministic bake of everything still wet (\"wet paint is\n * dried on save\": the document holds dried pixels, never wet state).\n * - **Undo.** Each wet step of history keeps a snapshot of the world before and after it, within\n * `undoMemory` bytes; older snapshots are dropped, and undoing past them leaves that paint dry\n * as it looked.\n */\n\nimport { dabRect, rasterizeDab } from '../paint/brush';\nimport { exchangeRates, PaintExchange, type MixerLoad, type RgbTriple } from '../paint/exchange';\nimport { pigmentOf, PIGMENT_UNIT } from '../paint/wet/film';\nimport { DEFAULT_FLOW, flowField, normalizeFlow, type WetFlowSpec } from '../paint/wet/flow';\nimport { WetMedium } from '../paint/wet/medium';\nimport { PaintGradientSampler, type PaintGradientSample, type PaintGradientSource } from '../paint/gradient-paint';\nimport { DEFAULT_WET_PARAMS, FLAT, normalizeTilt, normalizeWetParams, type WetParams, type WetTilt } from '../paint/wet/model';\nimport { createWetGpuEngine, findWebGpu, type WetGpuEngine, type WetGpuProvider, type WetGpuSupport } from '../paint/wet/gpu';\nimport { WetWorld, type WetBackendInfo, type WetLaneStats, type WetPaperInput, type WetSnapshot, type WetWorldOptions } from '../paint/wet/world';\nimport { TILE, TiledSurface, type RasterRect } from '../raster/surface';\nimport type { PaintBrush, WetMediumKind } from './brushes';\nimport { IDENTITY_COPY, PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine';\nimport { grainCatch, hexToRgb8, PaperGrain, type PaperGrain as PaperGrainType, type PaperSettings } from './paper';\n\n/** Water depth a stroke lays at the tip's centre at `water` 1 (the sum over its dabs). */\nconst STROKE_WATER = 0.9;\n/** Pigment mass a stroke lays at the tip's centre at `pigment` 1: a little more than a full-strength wash. */\nconst STROKE_MASS = PIGMENT_UNIT * 2.5;\n/** Coverage quantum: dab coverage is rounded to this before it reaches the simulation. */\nconst COVERAGE_STEPS = 4096;\n\nfunction union(a: RasterRect | null, b: RasterRect): RasterRect {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\n/** The paper as the wet simulation reads it. */\nexport function wetPaperInput(paper: PaperSettings): WetPaperInput {\n const grain = new PaperGrain(paper, 'wet');\n const flat = grain.flat;\n return {\n absorption: paper.absorption,\n permeability: paper.permeability,\n roughness: flat ? 0 : paper.roughness,\n color: hexToRgb8(paper.color),\n heightAt: flat ? () => 0.5 : (x, y) => grain.at(x, y),\n };\n}\n\nexport interface WetStrokeInit {\n world: WetWorld;\n laneId: string;\n brush: PaintBrush;\n /** Paint colour, sRGB 0..1. */\n color: RgbTriple;\n load: MixerLoad;\n grain: PaperGrainType | null;\n paperDepth: number;\n paperColor: readonly [number, number, number];\n seed: number;\n copies: readonly StrokeCopy[];\n /** Where paint may land (canvas-sized coverage 0..255: the stencils' mask), or none. */\n mask?: { width: number; height: number; data: ArrayLike<number> } | null;\n /** Gradient paint (resolved): each dab's pigment comes from it. */\n paint?: PaintGradientSource | null;\n}\n\ninterface CopyState {\n walker: PenDabWalker;\n exchange: PaintExchange | null;\n last: PenDab | null;\n}\n\n/**\n * One wet-brush stroke: pen samples → dabs → wet tool edits. Points are applied only while the\n * world is between batches (`flush` applies the ones that waited).\n */\nexport class WetStrokeDriver {\n dabCount = 0;\n bounds: RasterRect | null = null;\n /** The world's clock at which each raw point was applied (recorded with the stroke). */\n readonly steps: number[] = [];\n private readonly smoother: PenSmoother;\n private readonly states: CopyState[];\n private readonly medium: WetMedium;\n private readonly queue: PenPoint[] = [];\n private finishing = false;\n private finished = false;\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n private tip = new Float32Array(0);\n private cov = new Float32Array(0);\n private readonly paint: RgbTriple = [0, 0, 0];\n private readonly depth: number;\n\n constructor(readonly init: WetStrokeInit) {\n const brush = init.brush;\n this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);\n const copies = init.copies.length ? init.copies : [IDENTITY_COPY];\n const wet = brush.wet;\n const spacing = Math.max(0.02, brush.tip.spacing);\n // A stroke lays `STROKE_WATER × water` at the tip's centre: each dab a share by its spacing.\n const perDab = Math.min(1, spacing * 1.2);\n // Thinner: more water, less pigment and body.\n const thin = wet.thinner;\n this.medium = new WetMedium(init.world, init.laneId, {\n water: STROKE_WATER * Math.min(1, wet.water * (1 + thin)) * perDab,\n mass: STROKE_MASS * wet.pigment * (1 - 0.6 * thin) * perDab,\n body: wet.body * (1 - thin),\n });\n // Loading: below 1 the reservoir drains along the stroke, so the paint runs out.\n const rates = exchangeRates(brush.behavior.pickup, brush.behavior.mix, wet.load, brush.behavior.mixing);\n this.states = copies.map((c, i) => ({\n walker: new PenDabWalker(brush, (init.seed + i * 7919) | 0, c.linear),\n exchange: wet.tool === 'paint' ? new PaintExchange(rates, init.load) : null,\n last: null,\n }));\n this.copies = copies;\n this.depth = init.grain && !init.grain.flat ? Math.min(1, Math.max(0, init.paperDepth * brush.grain)) : 0;\n // Across the tip makes no sense for one wet dab: the colour follows the dab instead.\n const g = init.paint ?? null;\n this.sampler = g ? new PaintGradientSampler(g.map.by === 'across-tip' ? { ...g, map: { by: 'distance', length: 300, repeat: 'mirror' } } : g, init.seed) : null;\n }\n\n private readonly sampler: PaintGradientSampler | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n\n private readonly copies: readonly StrokeCopy[];\n\n /** What copy 0's brush carries after the dabs so far. */\n get load(): MixerLoad | null {\n return this.states[0].exchange?.load ?? null;\n }\n\n /** Whether every point (and the end) has been applied. */\n get done(): boolean {\n return this.finished;\n }\n\n /** Adds raw points; applies them now unless a batch is running. Returns where pixels changed. */\n push(points: readonly PenPoint[]): RasterRect[] {\n for (const p of points) this.queue.push(p);\n return this.flush();\n }\n\n /** Asks for the end; it happens now unless a batch is running (then at the next `flush`). */\n finish(): RasterRect[] {\n this.finishing = true;\n return this.flush();\n }\n\n /** Applies waiting points (and the end, when asked) if the world is between batches. */\n flush(): RasterRect[] {\n const rects: RasterRect[] = [];\n if (this.init.world.busy || this.finished) return rects;\n while (this.queue.length) {\n const raw = this.queue.shift()!;\n this.steps.push(this.init.world.clock);\n this.lastRaw = raw;\n const p = this.smoother.push(raw);\n this.lastFed = p;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n this.states[i].walker.push(c.map(p), dabs);\n this.applyDabs(i, dabs, rects);\n });\n }\n if (this.finishing) {\n const raw = this.lastRaw;\n const fed = this.lastFed;\n this.copies.forEach((c, i) => {\n const dabs: PenDab[] = [];\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);\n this.states[i].walker.finish(dabs);\n this.applyDabs(i, dabs, rects);\n });\n this.finished = true;\n }\n for (const r of rects) this.bounds = union(this.bounds, r);\n return rects;\n }\n\n private applyDabs(copy: number, dabs: PenDab[], rects: RasterRect[]): void {\n for (const d of dabs) {\n const rect = dabRect(d, this.init.brush.tip.hardness);\n if (rect.width <= 0 || rect.height <= 0) continue;\n this.dab(copy, d, rect);\n rects.push(rect);\n this.dabCount++;\n }\n }\n\n /** The tip's coverage over `rect`, with the grain, quantised. */\n private coverage(d: PenDab, rect: RasterRect): { tip: Float32Array; cov: Float32Array } {\n const n = rect.width * rect.height;\n if (this.tip.length < n) {\n this.tip = new Float32Array(n);\n this.cov = new Float32Array(n);\n }\n const tip = this.tip.subarray(0, n);\n const cov = this.cov.subarray(0, n);\n tip.fill(0);\n rasterizeDab({ ...d, alpha: 1 }, this.init.brush.tip.hardness, rect, tip);\n const grain = this.depth > 0 ? this.init.grain : null;\n const mask = this.init.mask;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < rect.width; xx++, k++) {\n if (mask && tip[k] > 0) {\n const mx = rect.x + xx;\n const my = rect.y + yy;\n tip[k] *= mx >= 0 && my >= 0 && mx < mask.width && my < mask.height ? mask.data[my * mask.width + mx] / 255 : 0;\n }\n let v = tip[k];\n if (v > 0 && grain) v *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, d.pressure);\n tip[k] = Math.round(tip[k] * COVERAGE_STEPS) / COVERAGE_STEPS;\n cov[k] = Math.round(v * COVERAGE_STEPS) / COVERAGE_STEPS;\n }\n }\n return { tip, cov };\n }\n\n private dab(copy: number, d: PenDab, rect: RasterRect): void {\n const { world, laneId, brush } = this.init;\n const wet = brush.wet;\n const st = this.states[copy];\n const { tip, cov } = this.coverage(d, rect);\n // How hard this dab works: flow, pressure (pressureOpacity) and the tip's opacity.\n const k = Math.round(Math.min(1, d.alpha * d.opacity * brush.tip.opacity) * COVERAGE_STEPS) / COVERAGE_STEPS;\n if (!(k > 0)) return;\n switch (wet.tool) {\n case 'paint': {\n const ex = st.exchange!;\n let flow = 1;\n if (this.sampler) {\n // Gradient paint: this dab's pigment.\n const g = this.sampler.dab(d, this.sample);\n ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);\n flow = g.alpha;\n }\n ex.pickUp(this.medium.sample(rect, tip));\n const total = ex.paint(this.paint);\n if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);\n else world.paint(laneId, rect, cov, this.medium.options.water * k, null);\n ex.drain();\n if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);\n break;\n }\n case 'water': {\n world.paint(laneId, rect, cov, this.medium.options.water * k * (wet.water > 0 ? 1 : 0), null);\n if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);\n break;\n }\n case 'dry':\n world.remove(laneId, rect, cov, wet.strength * 0.35 * k, 0, 0, wet.strength * 0.35 * k);\n break;\n case 'blow': {\n // Along the direction of travel (dab directions are counter-clockwise with y up).\n const s = wet.strength * 0.08 * k;\n world.push(laneId, rect, cov, Math.cos(d.direction) * s, -Math.sin(d.direction) * s);\n break;\n }\n case 'blend':\n world.blend(laneId, rect, cov, wet.strength * k);\n break;\n case 'smudge': {\n if (st.last) world.drag(laneId, rect, cov, d.x - st.last.x, d.y - st.last.y, wet.strength * k);\n break;\n }\n case 'blot':\n world.remove(laneId, rect, cov, wet.strength * 0.8 * k, wet.strength * 0.6 * k, wet.strength * 0.15 * k, wet.strength * 0.5 * k);\n break;\n }\n st.last = d;\n }\n}\n\n/** The wet settings each medium gives a layer the first time it holds wet paint. */\nexport const WET_MEDIUM_PARAMS: Readonly<Record<WetMediumKind, Partial<WetParams>>> = {\n watercolor: {},\n gouache: { viscosity: 0.45, edgeDarkening: 0.15, granulation: 0.1, bloom: 0.1, bleed: 0.15, pigmentWeight: 0.5 },\n oil: { viscosity: 0.95, evaporation: 0.03, edgeDarkening: 0, granulation: 0, bloom: 0, bleed: 0.08, pickup: 0.6, fixture: 0.02, pigmentWeight: 0.8 },\n};\n\n/** Settings of one wet layer (saved with the document). */\nexport interface WetLayerSettings {\n params: WetParams;\n flow: WetFlowSpec;\n}\n\n/** One wet layer as tools describe it. */\nexport interface WetLayerInfo {\n layerId: string;\n /** Surface water anywhere on the layer. */\n wet: boolean;\n /** Cells with surface water. */\n wetCells: number;\n /** Where the surface water is, or `null`. */\n wetBounds: RasterRect | null;\n /** 256 px tiles being simulated. */\n activeTiles: number;\n params: WetParams;\n flow: WetFlowSpec;\n}\n\n/** An event recorded with the clock it happened at (strokes are recorded separately), for replays. */\nexport interface WetEvent {\n clock: number;\n /** How many strokes were recorded before it (where it falls between strokes in a replay). */\n at?: number;\n kind: 'dry' | 'tilt' | 'params' | 'flow' | 'wet-paper';\n layerId?: string;\n data?: unknown;\n}\n\nexport interface PaintWetHost {\n canvas(): { width: number; height: number };\n paper(): PaperSettings;\n /** The layer's current pixels (`null` when it is not a pixel layer). */\n layerSurface(id: string): TiledSurface | null;\n /** Shows new pixels for wet layers (not an undo step); `rects` are where they changed. */\n updateSurfaces(surfaces: ReadonlyMap<string, TiledSurface>, rects: RasterRect[]): void;\n /** The paint dried while time passed (the editor records nothing, it just redraws). */\n changed(): void;\n}\n\nexport interface PaintWetOptions extends WetWorldOptions {\n /** Steps per second while time runs (default 120). */\n speed?: number;\n /** Bytes of wet snapshots kept for undo (default 512 MB). */\n undoMemory?: number;\n /**\n * Engine B, the WebGPU accelerator for large wet areas: `auto` (default: used when WebGPU is\n * there and at least `gpuMinTiles` tiles are wet) or `off` (the CPU does everything).\n */\n gpu?: WetGpuMode;\n /** Live 256 px tiles at which the GPU takes over (default 16, a fully wet 1024² canvas). */\n gpuMinTiles?: number;\n /** The WebGPU to use (default `navigator.gpu`; tests pass Node's). */\n gpuProvider?: WetGpuProvider | null;\n}\n\n/** Whether Engine B (WebGPU) may run: `auto`, or `off` to force the CPU. */\nexport type WetGpuMode = 'auto' | 'off';\nexport const WET_GPU_MODES: readonly WetGpuMode[] = ['auto', 'off'];\n\n/**\n * Engine B as panels and tools show it. `state`: `off` (forced to the CPU), `not-started` (it\n * starts with the first wet stroke), `starting`, `unavailable` (no usable WebGPU: see `reason`),\n * `standby` (ready; the wet area is below `minTiles`), `active` (the GPU shades the wet paint), or\n * `failed` (it stopped: see `reason`). The CPU stays the authority in every state.\n */\nexport interface WetEngineInfo {\n mode: WetGpuMode;\n state: 'off' | 'not-started' | 'starting' | 'unavailable' | 'standby' | 'active' | 'failed';\n reason: string | null;\n /** The GPU, when known. */\n adapter: string | null;\n /** Most wet tiles the GPU can hold. */\n maxTiles: number;\n /** Live tiles at which the GPU takes over. */\n minTiles: number;\n /** Live tiles now (all wet layers). */\n activeTiles: number;\n /** The CPU kernel (the authority), or `null` before the first wet stroke. */\n cpu: WetBackendInfo | null;\n}\n\n/** Steps the GPU may show ahead of the CPU between strokes (the longest CPU batch then). */\nexport const GPU_LEAD = 16;\n\n/** Tiles reshaded per frame at most (the rest catch up over the next frames). */\nconst DISPLAY_TILES_PER_TICK = 24;\n\n/** The paint editor's wet layers: one `WetWorld` per canvas, its settings and time. */\nexport class PaintWetLayers {\n world: WetWorld | null = null;\n tilt: WetTilt = { ...FLAT };\n readonly settings = new Map<string, WetLayerSettings>();\n paused = false;\n /** Wet strokes dry as soon as they end (one undo step with the stroke). */\n instantDry = false;\n speed: number;\n overlay = false;\n readonly undoMemory: number;\n /** The live wet stroke, if any. */\n driver: WetStrokeDriver | null = null;\n private lastTick: number | null = null;\n private ticking: Promise<void> | null = null;\n private stepsOwed = 0;\n private displaying: Promise<void> | null = null;\n private leadAt = 0;\n gpuMode: WetGpuMode;\n gpuMinTiles: number;\n private gpu: WetGpuEngine | null = null;\n private gpuSupport: WetGpuSupport | null = null;\n private gpuStarting: Promise<void> | null = null;\n\n constructor(\n private readonly host: PaintWetHost,\n private readonly options: PaintWetOptions = {}\n ) {\n this.speed = Math.max(1, Math.min(2000, options.speed ?? 120));\n this.undoMemory = Math.max(0, options.undoMemory ?? 512 * 1024 * 1024);\n this.gpuMode = options.gpu === 'off' ? 'off' : 'auto';\n this.gpuMinTiles = Math.max(1, Math.floor(options.gpuMinTiles ?? 16));\n }\n\n // --- Engine B ------------------------------------------------------------------\n\n /** Starts the GPU engine for the world (once; it may turn out unavailable). */\n private startGpu(): void {\n if (this.gpuMode === 'off' || this.gpu || this.gpuStarting || this.gpuSupport) return;\n const provider = this.options.gpuProvider === undefined ? findWebGpu() : this.options.gpuProvider;\n this.gpuStarting = createWetGpuEngine(provider)\n .then(({ engine, support }) => {\n this.gpuSupport = support;\n if (!engine) return;\n if (this.gpuMode === 'off' || !this.world) {\n engine.destroy();\n this.gpuSupport = null;\n return;\n }\n this.gpu = engine;\n this.world.gpuMinTiles = this.gpuMinTiles;\n this.world.attachGpu(engine);\n })\n .finally(() => {\n this.gpuStarting = null;\n this.host.changed();\n });\n }\n\n /** Resolves once the GPU engine has started or turned out unavailable. */\n async gpuReady(): Promise<WetEngineInfo> {\n if (this.gpuStarting) await this.gpuStarting;\n return this.engineInfo();\n }\n\n /** `auto`: use the GPU for large wet areas when there is one; `off`: the CPU only. */\n setGpu(mode: WetGpuMode, minTiles?: number): WetEngineInfo {\n if (minTiles !== undefined) {\n this.gpuMinTiles = Math.max(1, Math.floor(minTiles));\n if (this.world) this.world.gpuMinTiles = this.gpuMinTiles;\n }\n if (mode === this.gpuMode) return this.engineInfo();\n this.gpuMode = mode;\n if (mode === 'off') {\n this.world?.attachGpu(null);\n this.gpu?.destroy();\n this.gpu = null;\n this.gpuSupport = null;\n } else if (this.world) this.startGpu();\n return this.engineInfo();\n }\n\n engineInfo(): WetEngineInfo {\n const w = this.world;\n const base = { mode: this.gpuMode, adapter: this.gpuSupport?.adapter ?? null, maxTiles: this.gpuSupport?.maxSlots ?? 0, minTiles: this.gpuMinTiles, activeTiles: w?.activeTiles() ?? 0, cpu: this.backend() };\n if (this.gpuMode === 'off') return { ...base, state: 'off', reason: 'Forced to the CPU.' };\n if (this.gpuStarting) return { ...base, state: 'starting', reason: null };\n if (w?.gpuError) return { ...base, state: 'failed', reason: w.gpuError };\n if (this.gpuSupport && !this.gpuSupport.supported) return { ...base, state: 'unavailable', reason: this.gpuSupport.reason };\n if (!this.gpu) return { ...base, state: 'not-started', reason: 'Starts with the first wet stroke.' };\n if (w?.gpuActive) return { ...base, state: 'active', reason: null };\n return { ...base, state: 'standby', reason: `The GPU takes over at ${this.gpuMinTiles} wet tiles (256 px).` };\n }\n\n /** The world (made on first use, at the canvas size). */\n ensureWorld(): WetWorld {\n const { width, height } = this.host.canvas();\n if (this.world && (this.world.width !== width || this.world.height !== height)) throw new Error('The wet world is the wrong size; dry the paint before resizing the canvas.');\n if (!this.world) {\n this.world = new WetWorld(width, height, wetPaperInput(this.host.paper()), { kernel: this.options.kernel, threads: this.options.threads });\n this.world.setTilt(this.tilt);\n this.world.gpuMinTiles = this.gpuMinTiles;\n if (this.gpu) this.world.attachGpu(this.gpu);\n else this.startGpu();\n }\n return this.world;\n }\n\n /** Whether any layer (or `layerId`) has wet paint. */\n isWet(layerId?: string): boolean {\n const w = this.world;\n if (!w) return false;\n if (layerId !== undefined) return w.hasLane(layerId) && w.isActive(layerId);\n return w.laneIds().some((id) => w.isActive(id));\n }\n\n /** Layers holding a wet layer (wet or drying). */\n wetLayerIds(): string[] {\n return this.world?.laneIds() ?? [];\n }\n\n /** Whether a batch is running (edits wait). */\n get busy(): boolean {\n return Boolean(this.world?.busy);\n }\n\n /** Resolves when no batch is running. */\n async idle(): Promise<void> {\n while (this.ticking) await this.ticking;\n }\n\n settingsOf(layerId: string, medium: WetMediumKind = 'watercolor'): WetLayerSettings {\n let s = this.settings.get(layerId);\n if (!s) {\n s = { params: normalizeWetParams(WET_MEDIUM_PARAMS[medium], DEFAULT_WET_PARAMS), flow: { ...DEFAULT_FLOW } };\n this.settings.set(layerId, s);\n }\n return s;\n }\n\n /** Puts a layer in the world (its current pixels become the base under the wet paint). */\n ensureLane(layerId: string, medium?: WetMediumKind): WetWorld {\n const w = this.ensureWorld();\n if (!w.hasLane(layerId)) {\n const base = this.host.layerSurface(layerId) ?? new TiledSurface(4, new Map(), 0);\n const s = this.settingsOf(layerId, medium);\n w.ensureLane(layerId, { params: s.params, base });\n const { width, height } = this.host.canvas();\n if (s.flow.kind !== 'none') w.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);\n }\n return w;\n }\n\n setParams(layerId: string, update: Partial<WetParams>): WetParams {\n const s = this.settingsOf(layerId);\n s.params = normalizeWetParams(update, s.params);\n if (this.world?.hasLane(layerId)) this.world.setParams(layerId, s.params);\n return { ...s.params };\n }\n\n setFlow(layerId: string, update: Partial<WetFlowSpec>): WetFlowSpec {\n const s = this.settingsOf(layerId);\n s.flow = normalizeFlow(update, s.flow);\n if (this.world?.hasLane(layerId)) {\n const { width, height } = this.host.canvas();\n this.world.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);\n }\n return { ...s.flow };\n }\n\n setTilt(update: Partial<WetTilt>): WetTilt {\n this.tilt = normalizeTilt(update, this.tilt);\n this.world?.setTilt(this.tilt);\n return { ...this.tilt };\n }\n\n /** The paper changed. */\n paperChanged(): void {\n this.world?.setPaper(wetPaperInput(this.host.paper()));\n }\n\n /** A layer as tools describe it. */\n info(layerId: string): WetLayerInfo {\n const s = this.settingsOf(layerId);\n const st: WetLaneStats | null = this.world?.hasLane(layerId) ? this.world.stats(layerId) : null;\n return {\n layerId,\n wet: Boolean(st && st.wetCells > 0),\n wetCells: st?.wetCells ?? 0,\n wetBounds: st?.wetBounds ?? null,\n activeTiles: st?.activeTiles ?? 0,\n params: { ...s.params },\n flow: { ...s.flow },\n };\n }\n\n backend(): WetBackendInfo | null {\n return this.world?.backend() ?? null;\n }\n\n /** Shows the current wet pixels (at most `maxTiles` reshaded per layer). */\n syncDisplays(maxTiles = Infinity): RasterRect[] {\n const w = this.world;\n if (!w) return [];\n const surfaces = new Map<string, TiledSurface>();\n const rects: RasterRect[] = [];\n for (const id of w.laneIds()) {\n const d = w.display(id, maxTiles);\n if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);\n rects.push(...d.changed);\n }\n if (surfaces.size) this.host.updateSurfaces(surfaces, rects);\n return rects;\n }\n\n /** Like `syncDisplays`, shading on the GPU when Engine B is active (all stale tiles at once). */\n async syncDisplaysGpu(maxCpuTiles: number, lead = false): Promise<RasterRect[]> {\n const w = this.world;\n if (!w) return [];\n const surfaces = new Map<string, TiledSurface>();\n const rects: RasterRect[] = [];\n for (const id of w.laneIds()) {\n const d = await w.displayGpu(id, maxCpuTiles, lead);\n if (!w.hasLane(id)) continue;\n if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);\n rects.push(...d.changed);\n }\n if (surfaces.size) this.host.updateSurfaces(surfaces, rects);\n return rects;\n }\n\n /** Drops the wet layers whose paint has all dried (they become ordinary pixel layers). */\n private retireDry(): void {\n const w = this.world;\n if (!w) return;\n for (const id of w.laneIds()) {\n if (w.isActive(id) || this.driver?.init.laneId === id) continue;\n const base = w.getBase(id);\n w.removeLane(id);\n this.host.updateSurfaces(new Map([[id, base]]), []);\n }\n }\n\n /**\n * Advances time by the wall clock (`now` in ms, e.g. `performance.now()`): `speed` steps per\n * second, at most `maxSteps` per call. Call it every frame; it does nothing while paused or dry.\n * Resolves when the steps are done and the display is up to date.\n */\n tick(now: number, maxSteps = 8): Promise<void> {\n if (this.ticking) {\n // Between strokes, while the CPU works through a batch, the GPU shows its steps as they come.\n this.leadFrame(now);\n return this.ticking;\n }\n // The GPU is still shading the last frame: wait for it (edits need not; they just win).\n if (this.displaying) return this.displaying;\n const last = this.lastTick;\n this.lastTick = now;\n const w = this.world;\n if (!w || this.paused || last === null) {\n this.flushDriver();\n return Promise.resolve();\n }\n if (!w.isActive()) {\n this.flushDriver();\n this.retireDry();\n return Promise.resolve();\n }\n // Engine B between strokes: longer CPU batches (up to GPU_LEAD steps) that the GPU runs ahead through.\n const lead = w.gpuActive && !this.driver;\n w.gpuLeadMode = lead;\n const most = lead ? Math.max(maxSteps, GPU_LEAD) : maxSteps;\n this.stepsOwed = Math.min(most * 4, this.stepsOwed + ((now - last) / 1000) * this.speed);\n const n = Math.min(most, Math.floor(this.stepsOwed));\n this.leadAt = now;\n if (n < 1) {\n this.flushDriver();\n return Promise.resolve();\n }\n this.stepsOwed -= n;\n const run = (async () => {\n try {\n await w.step(n);\n } finally {\n this.ticking = null;\n }\n this.flushDriver();\n if (w.gpuActive) {\n const shading = this.syncDisplaysGpu(DISPLAY_TILES_PER_TICK).then(() => undefined);\n this.displaying = shading;\n try {\n await shading;\n } finally {\n this.displaying = null;\n }\n } else this.syncDisplays(DISPLAY_TILES_PER_TICK);\n this.retireDry();\n this.host.changed();\n })();\n this.ticking = run;\n return run;\n }\n\n /** Lead mode: steps the GPU on by the time since the last frame (within the running batch) and shows it. */\n private leadFrame(now: number): void {\n const w = this.world;\n if (!w || this.displaying || this.driver || !w.gpuLeading) return;\n const k = Math.max(1, Math.round(((now - this.leadAt) / 1000) * this.speed));\n if (!w.gpuLead(k)) return;\n this.leadAt = now;\n const shading = this.syncDisplaysGpu(0, true).then(() => this.host.changed());\n this.displaying = shading;\n void shading.finally(() => {\n if (this.displaying === shading) this.displaying = null;\n });\n }\n\n private flushDriver(): void {\n const d = this.driver;\n if (!d || !this.world) return;\n const rects = d.flush();\n if (rects.length) this.syncDisplays(DISPLAY_TILES_PER_TICK);\n if (d.done) this.onDriverDone?.(d);\n }\n\n /** Called when a live stroke that ended mid-batch has finished applying. */\n onDriverDone: ((driver: WetStrokeDriver) => void) | null = null;\n\n /** Steps `steps` times now (agents, tests). Returns the steps run. */\n async advance(steps: number): Promise<number> {\n await this.idle();\n const w = this.world;\n if (!w) return 0;\n // The fastest kernel (it gives the same bits; the TypeScript one is only a stand-in while it loads).\n await w.ready;\n await this.idle();\n const start = w.clock;\n const n = Math.max(0, Math.floor(steps));\n const job = (async () => {\n try {\n await w.step(n);\n } finally {\n this.ticking = null;\n }\n })();\n this.ticking = job;\n await job;\n if (w.gpuActive) await this.syncDisplaysGpu(Infinity);\n else this.syncDisplays();\n this.retireDry();\n return w.clock - start;\n }\n\n /**\n * Dries at once (everything settles where it is), on one layer or all, optionally only in a\n * rectangle. Returns the layers and rectangles that dried.\n */\n dry(layerId?: string, rect?: RasterRect): Array<{ layerId: string; bounds: RasterRect | null }> {\n const w = this.world;\n if (!w) return [];\n const ids = layerId !== undefined ? (w.hasLane(layerId) ? [layerId] : []) : w.laneIds();\n const out: Array<{ layerId: string; bounds: RasterRect | null }> = [];\n for (const id of ids) out.push({ layerId: id, bounds: w.dry(id, rect) });\n this.syncDisplays();\n this.retireDry();\n return out;\n }\n\n /** The layer's pixels with all its wet paint dried (deterministic), without changing anything. */\n bakedSurface(layerId: string): TiledSurface | null {\n const w = this.world;\n if (!w?.hasLane(layerId)) return null;\n return w.bakedSurface(layerId);\n }\n\n /** A snapshot of the world for undo (`null` when there is none). */\n snapshot(): WetSnapshot | null {\n const w = this.world;\n if (!w || !w.laneIds().length) return null;\n return w.snapshot();\n }\n\n /** Puts a snapshot back (`null`: no wet layers). Returns the layer surfaces it implies. */\n restore(snap: WetSnapshot | null): void {\n if (!snap) {\n if (this.world) for (const id of this.world.laneIds()) this.world.removeLane(id);\n return;\n }\n this.ensureWorld().restore(snap);\n this.syncDisplays();\n }\n\n /** Forgets everything wet (a new canvas, a resize, opening a document). */\n reset(): void {\n this.world?.destroy();\n this.world = null;\n this.driver = null;\n this.settings.clear();\n this.lastTick = null;\n this.stepsOwed = 0;\n }\n\n /** Water overlay tiles (RGBA) over a document rectangle, for the view. */\n waterOverlay(rect: RasterRect): Array<{ tx: number; ty: number; data: Uint8ClampedArray }> {\n const w = this.world;\n if (!w) return [];\n const out: Array<{ tx: number; ty: number; data: Uint8ClampedArray }> = [];\n const tx0 = Math.max(0, Math.floor(rect.x / TILE));\n const ty0 = Math.max(0, Math.floor(rect.y / TILE));\n const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);\n const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);\n for (let ty = ty0; ty <= ty1; ty++)\n for (let tx = tx0; tx <= tx1; tx++) {\n let merged: Uint8ClampedArray | null = null;\n for (const id of w.laneIds()) {\n const t = w.waterOverlay(id, tx, ty);\n if (!t) continue;\n if (!merged) merged = t;\n else for (let i = 3; i < t.length; i += 4) merged[i] = Math.max(merged[i], t[i]);\n }\n if (merged) out.push({ tx, ty, data: merged });\n }\n return out;\n }\n\n destroy(): void {\n this.world?.destroy();\n this.world = null;\n this.gpu?.destroy();\n this.gpu = null;\n }\n}\n\n/** The pigment (K r, g, b, S) of a colour at a body, per unit mass (for tools that report it). */\nexport function wetPigmentOf(hex: string, body: number): number[] {\n const [r, g, b] = hexToRgb8(hex);\n return Array.from(pigmentOf([r / 255, g / 255, b / 255], body));\n}\n","/**\n * Brush thumbnails: a test stroke (an S-curve with pressure swelling and fading, at a steady\n * speed) painted with the brush on the paper. Brushes that only move paint (blender, smudge) are\n * shown dragged across two bands of colour. Pure TypeScript, so thumbnails render in Node too.\n */\n\nimport { EMPTY_MIXER_LOAD } from '../paint/mixer';\nimport { emptySurface, fillRect, type TiledSurface } from '../raster/surface';\nimport { normalizePaintBrush, type PaintBrush } from './brushes';\nimport type { PenPoint } from './engine';\nimport { IDENTITY_COPY } from './engine';\nimport { hexToRgb8, type PaperSettings } from './paper';\nimport { createPaintCompositor, paperGrain, paperNode, type ViewLayer } from './render';\nimport { brushLoadForStroke, PaintStrokeSession } from './stroke';\nimport { WetWorld } from '../paint/wet/world';\nimport { pigmentOf } from '../paint/wet/film';\nimport { WetStrokeDriver, wetPaperInput } from './wet';\nimport { DEFAULT_LIGHTING, emptyThickChannels, LitCache, type ThickChannels } from './thick';\n\n/** One small wet world reused for wet brush thumbnails (by size). */\nlet thumbWorld: WetWorld | null = null;\n\n/** A wet brush's test stroke: laid on a wet layer (over a wet band of colour for the tools that move paint), shown as painted, before any time passes. */\nfunction paintWetTestStroke(brush: PaintBrush, paper: PaperSettings, color: string, width: number, height: number): TiledSurface {\n if (!thumbWorld || thumbWorld.width !== width || thumbWorld.height !== height) {\n thumbWorld?.destroy();\n thumbWorld = new WetWorld(width, height, wetPaperInput(paper), { kernel: 'typescript', threads: 0 });\n } else thumbWorld.setPaper(wetPaperInput(paper));\n const w = thumbWorld;\n if (w.hasLane('thumb')) w.removeLane('thumb');\n w.ensureLane('thumb');\n const [r, g, b] = hexToRgb8(color);\n if (brush.wet.tool !== 'paint' && brush.wet.tool !== 'water') {\n // Wet paint to work in: the colour over ochre, as for the dry brushes that move paint.\n const half = Math.ceil(height / 2);\n w.paint('thumb', { x: 0, y: 0, width, height: half }, null, 0.4, Array.from(pigmentOf([r / 255, g / 255, b / 255], 0), (v) => v * 0.05));\n w.paint('thumb', { x: 0, y: half, width, height: height - half }, null, 0.4, Array.from(pigmentOf([232 / 255, 196 / 255, 64 / 255], 0), (v) => v * 0.05));\n }\n const driver = new WetStrokeDriver({\n world: w,\n laneId: 'thumb',\n brush,\n color: [r / 255, g / 255, b / 255],\n load: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),\n grain: paperGrain(paper),\n paperDepth: paper.depth,\n paperColor: hexToRgb8(paper.color),\n seed: 7,\n copies: [IDENTITY_COPY],\n });\n driver.push(thumbnailStroke(width, height));\n driver.finish();\n // Blowing only shows once the water has moved.\n if (brush.wet.tool === 'blow') w.stepSync(4);\n return w.display('thumb').surface;\n}\n\nexport interface BrushThumbnail {\n width: number;\n height: number;\n /** RGBA, opaque (the paper is under the stroke). */\n data: Uint8ClampedArray;\n}\n\n/** The test stroke's samples for a `width × height` thumbnail. */\nexport function thumbnailStroke(width: number, height: number): PenPoint[] {\n const pts: PenPoint[] = [];\n const n = 64;\n const mx = width * 0.1;\n const my = height * 0.5;\n for (let i = 0; i <= n; i++) {\n const t = i / n;\n const pressure = t < 0.5 ? 0.15 + 1.7 * t : 1 - 1.4 * (t - 0.5);\n pts.push({ x: mx + (width - 2 * mx) * t, y: my - Math.sin(t * Math.PI * 2) * height * 0.22, pressure, t: i * 12 });\n }\n return pts;\n}\n\n/** Paints the test stroke with `brush` onto `surface`; returns the new pixels. */\nfunction paintTestStroke(surface: TiledSurface, brush: PaintBrush, paper: PaperSettings, color: string, width: number, height: number): TiledSurface {\n const [r, g, b] = hexToRgb8(color);\n let result = surface;\n let thick: ThickChannels | null = null;\n const session = new PaintStrokeSession(\n {\n update: () => {},\n commit: (sess, s) => {\n result = s ?? result;\n thick = sess.thickChannels();\n },\n },\n {\n layerId: 'thumbnail',\n surface,\n offset: { x: 0, y: 0 },\n lockTransparency: false,\n brush,\n color: [r, g, b, 255],\n background: [...hexToRgb8(paper.color), 255],\n grain: paperGrain(paper),\n paperDepth: paper.depth,\n seed: 7,\n colorHex: color,\n affineCopies: [],\n copies: [IDENTITY_COPY],\n mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),\n thick: brush.engine === 'impasto' ? { channels: emptyThickChannels() } : undefined,\n sticker: brush.engine === 'sticker' ? { nextId: 1 } : undefined,\n }\n );\n session.push(thumbnailStroke(width, height));\n session.end();\n // Thick paint is shown lit, as on the canvas.\n return thick ? new LitCache().lit(result, thick, DEFAULT_LIGHTING) : result;\n}\n\nexport function renderBrushThumbnail(brush: PaintBrush, options: { paper: PaperSettings; width?: number; height?: number; color: string }): BrushThumbnail {\n const width = Math.max(16, Math.min(512, Math.round(options.width ?? 160)));\n const height = Math.max(16, Math.min(512, Math.round(options.height ?? 48)));\n // Fit the tip in the thumbnail: big brushes are drawn smaller.\n const maxSize = height * 0.42;\n const b = brush.tip.size > maxSize ? normalizePaintBrush({ tip: { ...brush.tip, size: maxSize } }, brush) : brush;\n const rgba = [...hexToRgb8(options.color), 255];\n let surface: TiledSurface = emptySurface(4);\n if (b.engine === 'eraser') {\n // Something to erase: a band of the colour.\n surface = fillRect(surface, { x: 0, y: 0, width, height }, rgba);\n } else if (b.engine === 'wet') {\n // the wet layer draws its own band\n } else if (b.engine === 'blender' || b.engine === 'smudge' || (b.engine === 'mixer' && !b.loadsColor) || b.behavior.pickup > 0) {\n // Something to move: two bands, the colour over ochre.\n const half = Math.ceil(height / 2);\n surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);\n surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);\n }\n surface = b.engine === 'wet' ? paintWetTestStroke(b, options.paper, options.color, width, height) : paintTestStroke(surface, b, options.paper, options.color, width, height);\n const layer: ViewLayer = { type: 'pixel', id: 'thumbnail', name: 'thumbnail', visible: true, opacity: 1, blend: 'normal', surface, offset: { x: 0, y: 0 } };\n const data = createPaintCompositor().composite([paperNode(options.paper), layer], { x: 0, y: 0, width, height }, { width, height });\n return { width, height, data };\n}\n","// Portions ported from photocraft/crates/paint/src/symmetry.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Painting guides: mirror and kaleidoscope symmetry (every stroke paints its reflected or rotated\n * copies as one stroke), perspective guides with one to three vanishing points that strokes snap\n * to, a tracing image under the paint (optionally the source of the stroke colour), and floating\n * reference images to pick colours from. Geometry only: no pixels, no DOM.\n */\n\nimport { composeAffine, type Affine } from './strokes';\nimport type { Stencil } from './stencils';\n\nexport type SymmetryMode = 'off' | 'vertical' | 'horizontal' | 'dual' | 'diagonal' | 'kaleidoscope';\n\nexport const SYMMETRY_MODES: readonly SymmetryMode[] = ['off', 'vertical', 'horizontal', 'dual', 'diagonal', 'kaleidoscope'];\n\nexport interface SymmetryGuide {\n mode: SymmetryMode;\n /** Centre of the axes in document pixels (default the canvas centre). */\n center: { x: number; y: number };\n /** Turns the axes, degrees clockwise (default 0). */\n rotation: number;\n /** Kaleidoscope: number of segments, 2..32 (default 6). */\n segments: number;\n /** Kaleidoscope: reflect every other segment as well as rotating (default true), like a real kaleidoscope. */\n mirror: boolean;\n}\n\nexport interface Point2 {\n x: number;\n y: number;\n}\n\nexport interface PerspectiveGuide {\n enabled: boolean;\n /** One to three vanishing points in document pixels (they may lie outside the canvas). */\n points: Point2[];\n /** Strokes snap to the guide lines (default true when enabled). */\n snap: boolean;\n}\n\n/** A view-only image under the paint, placed over the canvas. */\nexport interface TracingImage {\n /** PNG or JPEG data URL. */\n src: string;\n /** Natural size of the image in pixels. */\n width: number;\n height: number;\n /** Where it is drawn, in document pixels (default: fitted to the canvas). */\n rect: { x: number; y: number; width: number; height: number };\n /** 0..1 (default 0.35). */\n opacity: number;\n visible: boolean;\n /** Strokes without an explicit colour take the tracing image's colour where they start. */\n sampleColors: boolean;\n}\n\n/** A floating image to look at and pick colours from (shown beside the canvas, never painted). */\nexport interface ReferenceImage {\n id: string;\n name: string;\n src: string;\n width: number;\n height: number;\n /** Where the panel floats over the viewport, in screen pixels, and its displayed width. */\n x: number;\n y: number;\n displayWidth: number;\n visible: boolean;\n}\n\nexport interface PaintGuides {\n symmetry: SymmetryGuide;\n perspective: PerspectiveGuide;\n tracing: TracingImage | null;\n references: ReferenceImage[];\n /** Rulers, arcs, straight edges and shape templates that guide or mask strokes. */\n stencils: Stencil[];\n}\n\nexport function defaultGuides(canvas: { width: number; height: number }): PaintGuides {\n return {\n symmetry: { mode: 'off', center: { x: canvas.width / 2, y: canvas.height / 2 }, rotation: 0, segments: 6, mirror: true },\n perspective: { enabled: false, points: [], snap: true },\n tracing: null,\n references: [],\n stencils: [],\n };\n}\n\nexport const MAX_COORDINATE = 1_000_000;\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst coord = (v: unknown, d: number) => (finite(v) && Math.abs(v) <= MAX_COORDINATE ? v : d);\n\nexport function normalizeSymmetry(raw: Partial<SymmetryGuide> | Record<string, unknown>, base: SymmetryGuide): SymmetryGuide {\n const r = raw as Record<string, unknown>;\n const c = (r.center ?? {}) as Record<string, unknown>;\n return {\n mode: SYMMETRY_MODES.includes(r.mode as SymmetryMode) ? (r.mode as SymmetryMode) : base.mode,\n center: { x: coord(c.x, base.center.x), y: coord(c.y, base.center.y) },\n rotation: finite(r.rotation) ? ((r.rotation % 360) + 360) % 360 : base.rotation,\n segments: finite(r.segments) ? Math.min(32, Math.max(2, Math.round(r.segments))) : base.segments,\n mirror: typeof r.mirror === 'boolean' ? r.mirror : base.mirror,\n };\n}\n\n/** Reflection across the line through `c` in direction angle `a` (radians, clockwise with y down). */\nfunction reflectAbout(c: Point2, a: number): Affine {\n const dx = Math.cos(a);\n const dy = Math.sin(a);\n // 2·d·dᵀ − I, conjugated by the translation to the centre.\n const m00 = 2 * dx * dx - 1;\n const m01 = 2 * dx * dy;\n const m11 = 2 * dy * dy - 1;\n return [m00, m01, m01, m11, c.x - (m00 * c.x + m01 * c.y), c.y - (m01 * c.x + m11 * c.y)];\n}\n\n/** Rotation by `a` radians (clockwise with y down) about `c`. */\nfunction rotateAbout(c: Point2, a: number): Affine {\n const cos = Math.cos(a);\n const sin = Math.sin(a);\n return [cos, -sin, sin, cos, c.x - (cos * c.x - sin * c.y), c.y - (sin * c.x + cos * c.y)];\n}\n\nconst rad = (deg: number) => (deg * Math.PI) / 180;\n\n/** The axis directions of a mirror mode (radians), as photocraft's `directions`. */\nexport function symmetryAxisAngles(g: SymmetryGuide): number[] {\n if (g.mode === 'off') return [];\n if (g.mode === 'kaleidoscope') return Array.from({ length: g.segments }, (_, k) => rad(g.rotation) + (k * Math.PI) / g.segments);\n const initial = g.mode === 'horizontal' ? 0 : g.mode === 'diagonal' ? 45 : 90;\n const a = rad(initial + g.rotation);\n return g.mode === 'dual' ? [a, a + Math.PI / 2] : [a];\n}\n\n/**\n * The extra copies a stroke paints under the symmetry guide, as affine maps (the stroke itself is\n * not included). Vertical, horizontal and diagonal: one reflection. Dual: two reflections and the\n * point reflection. Kaleidoscope: rotations by 360°/n, and with `mirror` the reflections between them.\n */\nexport function symmetryCopies(g: SymmetryGuide): Affine[] {\n const c = g.center;\n switch (g.mode) {\n case 'off':\n return [];\n case 'vertical':\n case 'horizontal':\n case 'diagonal':\n return symmetryAxisAngles(g).map((a) => reflectAbout(c, a));\n case 'dual': {\n const [a, b] = symmetryAxisAngles(g);\n return [reflectAbout(c, a), reflectAbout(c, b), rotateAbout(c, Math.PI)];\n }\n case 'kaleidoscope': {\n const out: Affine[] = [];\n const n = g.segments;\n for (let k = 1; k < n; k++) out.push(rotateAbout(c, (2 * Math.PI * k) / n));\n if (g.mirror) {\n const first = reflectAbout(c, rad(g.rotation));\n for (let k = 0; k < n; k++) out.push(composeAffine(rotateAbout(c, (2 * Math.PI * k) / n), first));\n }\n return out;\n }\n }\n}\n\n/** The axes clipped to the canvas, for drawing (photocraft's `segments`). */\nexport function symmetryAxisSegments(g: SymmetryGuide, size: { width: number; height: number }): Array<[Point2, Point2]> {\n const out: Array<[Point2, Point2]> = [];\n for (const a of symmetryAxisAngles(g)) {\n const d = [Math.cos(a), Math.sin(a)];\n const origin = [g.center.x, g.center.y];\n const extent = [size.width, size.height];\n let lo = -Infinity;\n let hi = Infinity;\n let ok = true;\n for (let i = 0; i < 2; i++) {\n if (Math.abs(d[i]) < 1e-12) {\n if (origin[i] < 0 || origin[i] > extent[i]) ok = false;\n } else {\n const t0 = -origin[i] / d[i];\n const t1 = (extent[i] - origin[i]) / d[i];\n lo = Math.max(lo, Math.min(t0, t1));\n hi = Math.min(hi, Math.max(t0, t1));\n }\n }\n if (!ok || !Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo) continue;\n out.push([{ x: g.center.x + lo * d[0], y: g.center.y + lo * d[1] }, { x: g.center.x + hi * d[0], y: g.center.y + hi * d[1] }]);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Perspective\n// ---------------------------------------------------------------------------\n\nexport function normalizePerspective(raw: Partial<PerspectiveGuide> | Record<string, unknown>, base: PerspectiveGuide): PerspectiveGuide {\n const r = raw as Record<string, unknown>;\n let points = base.points;\n if (Array.isArray(r.points)) {\n if (r.points.length > 3) throw new Error(`A perspective guide has one to three vanishing points; got ${r.points.length}.`);\n points = r.points.map((p, i) => {\n const q = (p ?? {}) as Record<string, unknown>;\n if (!finite(q.x) || !finite(q.y) || Math.abs(q.x) > MAX_COORDINATE || Math.abs(q.y) > MAX_COORDINATE) {\n throw new Error(`points[${i}] needs finite x and y within ±${MAX_COORDINATE} document pixels.`);\n }\n return { x: q.x, y: q.y };\n });\n }\n const enabled = typeof r.enabled === 'boolean' ? r.enabled : base.enabled;\n if (enabled && !points.length) throw new Error('An enabled perspective guide needs at least one vanishing point; pass points.');\n return { enabled, points, snap: typeof r.snap === 'boolean' ? r.snap : base.snap };\n}\n\n/** The line families a perspective guide snaps to at `p`: unit directions. */\nexport function perspectiveDirections(g: PerspectiveGuide, p: Point2): Point2[] {\n const out: Point2[] = [];\n for (const v of g.points) {\n const dx = v.x - p.x;\n const dy = v.y - p.y;\n const len = Math.hypot(dx, dy);\n if (len > 1e-6) out.push({ x: dx / len, y: dy / len });\n }\n // One-point perspective keeps true horizontals and verticals; two-point keeps verticals.\n if (g.points.length <= 2) out.push({ x: 0, y: 1 });\n if (g.points.length <= 1) out.push({ x: 1, y: 0 });\n return out;\n}\n\n/** How far the pen must move before a stroke chooses its perspective line (document pixels). */\nexport const SNAP_DECIDE_DISTANCE = 6;\n\n/**\n * Snaps one stroke to a perspective guide: once the pen has moved `SNAP_DECIDE_DISTANCE` from\n * where it started, the stroke picks the guide line through its start that best matches its\n * direction, and every sample after that is projected onto the line.\n */\nexport class PerspectiveSnapper {\n private start: Point2 | null = null;\n private dir: Point2 | null = null;\n\n constructor(private readonly guide: PerspectiveGuide) {}\n\n /** The line the stroke follows, once chosen. */\n get line(): { from: Point2; direction: Point2 } | null {\n return this.start && this.dir ? { from: this.start, direction: this.dir } : null;\n }\n\n snap<P extends Point2>(p: P): P {\n if (!this.guide.enabled || !this.guide.snap || !this.guide.points.length) return p;\n if (!this.start) {\n this.start = { x: p.x, y: p.y };\n return p;\n }\n const s = this.start;\n if (!this.dir) {\n const dx = p.x - s.x;\n const dy = p.y - s.y;\n const len = Math.hypot(dx, dy);\n if (len < SNAP_DECIDE_DISTANCE) return { ...p, x: s.x, y: s.y };\n let best: Point2 | null = null;\n let bestDot = -1;\n for (const d of perspectiveDirections(this.guide, s)) {\n const dot = Math.abs((dx * d.x + dy * d.y) / len);\n if (dot > bestDot) {\n bestDot = dot;\n best = d;\n }\n }\n this.dir = best;\n if (!best) return p;\n }\n const d = this.dir!;\n const t = (p.x - s.x) * d.x + (p.y - s.y) * d.y;\n return { ...p, x: s.x + d.x * t, y: s.y + d.y * t };\n }\n}\n\n/** Guide lines to draw for a perspective guide: rays from each vanishing point through the canvas. */\nexport function perspectiveGuideLines(g: PerspectiveGuide, size: { width: number; height: number }, count = 12): Array<[Point2, Point2]> {\n const out: Array<[Point2, Point2]> = [];\n const far = Math.hypot(size.width, size.height) * 4;\n for (const v of g.points) {\n // Aim the fan at the canvas: spread around the direction to its centre.\n const cx = size.width / 2 - v.x;\n const cy = size.height / 2 - v.y;\n const inside = v.x >= 0 && v.y >= 0 && v.x <= size.width && v.y <= size.height;\n const mid = Math.atan2(cy, cx);\n const spread = inside ? Math.PI : Math.min(Math.PI, 2 * Math.atan2(Math.hypot(size.width, size.height) / 2, Math.hypot(cx, cy)));\n for (let i = 0; i < count; i++) {\n const a = inside ? (2 * Math.PI * i) / count : mid - spread / 2 + (spread * (i + 0.5)) / count;\n out.push([{ ...v }, { x: v.x + Math.cos(a) * far, y: v.y + Math.sin(a) * far }]);\n }\n }\n return out;\n}\n","/**\n * Colour for painters: hex / RGB / HSV / HSL conversions, a colour wheel that is either the\n * painter's red-yellow-blue wheel (where red's complement is green) or the RGB wheel (where it is\n * cyan), harmonies on it, and swatches.\n */\n\nimport { paintSourceColor, readPaintSource, type PaintGradientSource } from '../paint/gradient-paint';\n\nexport type Rgb255 = [number, number, number];\n\nconst HEX = /^#?([0-9a-f]{6})$/i;\n\nexport function parseHexColor(value: unknown, what = 'color'): Rgb255 {\n const m = typeof value === 'string' ? HEX.exec(value.trim()) : null;\n if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. \"#2a4b8d\".`);\n const n = parseInt(m[1], 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nexport function rgbToHexColor(r: number, g: number, b: number): string {\n return '#' + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('');\n}\n\n/** RGB 0..255 → HSV with h in degrees 0..360, s and v 0..1. */\nexport function rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {\n const R = r / 255;\n const G = g / 255;\n const B = b / 255;\n const max = Math.max(R, G, B);\n const min = Math.min(R, G, B);\n const d = max - min;\n let h = 0;\n if (d > 0) {\n if (max === R) h = 60 * (((G - B) / d) % 6);\n else if (max === G) h = 60 * ((B - R) / d + 2);\n else h = 60 * ((R - G) / d + 4);\n }\n if (h < 0) h += 360;\n return { h, s: max === 0 ? 0 : d / max, v: max };\n}\n\nexport function hsvToRgb(h: number, s: number, v: number): Rgb255 {\n const hh = (((h % 360) + 360) % 360) / 60;\n const c = v * s;\n const x = c * (1 - Math.abs((hh % 2) - 1));\n const m = v - c;\n const [r, g, b] = hh < 1 ? [c, x, 0] : hh < 2 ? [x, c, 0] : hh < 3 ? [0, c, x] : hh < 4 ? [0, x, c] : hh < 5 ? [x, 0, c] : [c, 0, x];\n return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];\n}\n\n/** RGB 0..255 → HSL with h in degrees, s and l 0..1. */\nexport function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n const { h } = rgbToHsv(r, g, b);\n const max = Math.max(r, g, b) / 255;\n const min = Math.min(r, g, b) / 255;\n const l = (max + min) / 2;\n const d = max - min;\n return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };\n}\n\nexport function hslToRgb(h: number, s: number, l: number): Rgb255 {\n const c = (1 - Math.abs(2 * l - 1)) * s;\n const v = l + c / 2;\n return hsvToRgb(h, v === 0 ? 0 : c / v, v);\n}\n\n/** Which wheel hues live on: `ryb` (painter's: red ↔ green complements) or `rgb` (red ↔ cyan). */\nexport type ColorWheel = 'ryb' | 'rgb';\n\n// RYB wheel angle → RGB hue (a piecewise-linear map through red, orange, yellow, green, blue, violet).\nconst RYB_STOPS: ReadonlyArray<[number, number]> = [\n [0, 0],\n [60, 30],\n [120, 60],\n [180, 120],\n [240, 240],\n [300, 280],\n [360, 360],\n];\n\nfunction piecewise(v: number, from: 0 | 1): number {\n const x = ((v % 360) + 360) % 360;\n const to = from === 0 ? 1 : 0;\n for (let i = 1; i < RYB_STOPS.length; i++) {\n const a = RYB_STOPS[i - 1];\n const b = RYB_STOPS[i];\n if (x <= b[from]) return a[to] + ((x - a[from]) / (b[from] - a[from])) * (b[to] - a[to]);\n }\n return x;\n}\n\n/** A wheel angle (degrees) → an RGB hue. */\nexport function wheelToHue(angle: number, wheel: ColorWheel): number {\n return wheel === 'rgb' ? ((angle % 360) + 360) % 360 : piecewise(angle, 0);\n}\n\n/** An RGB hue → its wheel angle (degrees). */\nexport function hueToWheel(hue: number, wheel: ColorWheel): number {\n return wheel === 'rgb' ? ((hue % 360) + 360) % 360 : piecewise(hue, 1);\n}\n\nexport type HarmonyKind = 'complementary' | 'split-complementary' | 'analogous' | 'triadic' | 'tetradic' | 'square' | 'monochromatic';\n\nexport const HARMONY_KINDS: readonly HarmonyKind[] = ['complementary', 'split-complementary', 'analogous', 'triadic', 'tetradic', 'square', 'monochromatic'];\n\nexport interface HarmonyColor {\n color: string;\n /** Position on the wheel, degrees. */\n angle: number;\n}\n\n/**\n * The colours of a harmony around `base`, the base first. Hue-based harmonies keep the base's\n * saturation and value and turn it around the wheel; `analogous` spreads by `spread` degrees\n * (default 30); `monochromatic` keeps the hue and steps the lightness.\n */\nexport function colorHarmony(base: string, kind: HarmonyKind, options: { wheel?: ColorWheel; spread?: number } = {}): HarmonyColor[] {\n const wheel = options.wheel ?? 'ryb';\n const [r, g, b] = parseHexColor(base, 'base');\n const hsv = rgbToHsv(r, g, b);\n const start = hueToWheel(hsv.h, wheel);\n const at = (angle: number): HarmonyColor => {\n const a = ((angle % 360) + 360) % 360;\n const [rr, gg, bb] = hsvToRgb(wheelToHue(a, wheel), hsv.s, hsv.v);\n return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(a * 10) / 10 };\n };\n const spread = options.spread ?? 30;\n const offsets: Record<Exclude<HarmonyKind, 'monochromatic'>, number[]> = {\n complementary: [0, 180],\n 'split-complementary': [0, 180 - spread, 180 + spread],\n analogous: [0, -spread, spread],\n triadic: [0, 120, 240],\n tetradic: [0, 60, 180, 240],\n square: [0, 90, 180, 270],\n };\n if (kind === 'monochromatic') {\n const hsl = rgbToHsl(r, g, b);\n const steps = [0, -0.3, -0.15, 0.15, 0.3];\n return steps.map((d) => {\n const [rr, gg, bb] = d === 0 ? [r, g, b] : hslToRgb(hsl.h, hsl.s, Math.min(0.97, Math.max(0.03, hsl.l + d)));\n return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(start * 10) / 10 };\n });\n }\n return offsets[kind].map((o, i) => (i === 0 ? { color: rgbToHexColor(r, g, b), angle: Math.round(start * 10) / 10 } : at(start + o)));\n}\n\nexport interface Swatch {\n /** #rrggbb (for a gradient swatch: its first colour, what plain colour slots show). */\n color: string;\n name?: string;\n /** Gradient paint held by the swatch (`setPaint` picks it); missing for a colour swatch. */\n paint?: PaintGradientSource;\n}\n\n/** A swatch read from JSON, or `null` when it is not one (a gradient swatch keeps its gradient). */\nexport function readSwatch(raw: unknown): Swatch | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const g = readPaintSource(r.paint);\n const color = typeof r.color === 'string' && /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : g?.type === 'gradient' ? paintSourceColor(g) : null;\n if (!color) return null;\n const out: Swatch = { color };\n if (typeof r.name === 'string' && r.name.trim()) out.name = r.name.trim().slice(0, 80);\n if (g?.type === 'gradient') out.paint = g;\n return out;\n}\n\n/** A starting palette of our own generic paint colours. */\nexport const DEFAULT_SWATCHES: readonly Swatch[] = [\n { color: '#f7f5ee', name: 'Warm white' },\n { color: '#f3d23a', name: 'Lemon yellow' },\n { color: '#c8912f', name: 'Yellow ochre' },\n { color: '#d23a2a', name: 'Warm red' },\n { color: '#9e1f3d', name: 'Crimson' },\n { color: '#8a4a2b', name: 'Burnt earth' },\n { color: '#2f3f9e', name: 'Deep blue' },\n { color: '#1d6f8f', name: 'Cool blue' },\n { color: '#4f6e2c', name: 'Leaf green' },\n { color: '#25221f', name: 'Warm black' },\n];\n","/**\n * The mixing pad: a small canvas beside the painting to put paint on, smear it with a palette\n * knife and pick the mixed colour from. Paint is laid with the Mixer Brush in pigment mode and the\n * knife smears it with pigment mixing, so blue and yellow pushed together make green. It is\n * scratch space: not part of the painting's undo history, but saved with the document, and pads\n * can be kept by name (`MixingPad.toPng` / `fromPng`).\n *\n * `mix` puts measured blobs of paint down (areas in proportion to their parts) and works them\n * together with the knife, the way a painter mixes a pile on the palette.\n */\n\nimport { EMPTY_MIXER_LOAD } from '../paint/mixer';\nimport { mixPigments } from '../paint/pigment';\nimport { base64ToBytes, decodePng, isPng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport { emptySurface, readRegion, surfaceFromPixels, writeRegion, type TiledSurface } from '../raster/surface';\nimport { normalizePaintBrush, type PaintBrush } from './brushes';\nimport { IDENTITY_COPY, type PenPoint } from './engine';\nimport { hexToRgb8 } from './paper';\nimport { brushLoadForStroke, PaintStrokeSession } from './stroke';\n\nexport type MixingPadTool = 'paint' | 'knife';\n\nexport const MIXING_PAD_WIDTH = 360;\nexport const MIXING_PAD_HEIGHT = 220;\n\nconst PAD_PAINT: PaintBrush = normalizePaintBrush({\n id: 'pad-paint',\n name: 'Pad paint',\n category: 'mixer',\n engine: 'mixer',\n tip: { size: 28, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 0.7, pressureSize: false, pressureOpacity: false },\n mixer: { wet: 0.45, load: 1, mix: 0.4, loadEachStroke: true, cleanEachStroke: true, mixing: 'pigment' },\n});\n\nconst PAD_KNIFE: PaintBrush = normalizePaintBrush({\n id: 'pad-knife',\n name: 'Palette knife',\n category: 'smudge',\n engine: 'smudge',\n loadsColor: false,\n tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow: 1, pressureSize: false, pressureOpacity: false },\n // A knife drags streaks of paint and folds them into each other, mixing them as pigment.\n smudge: { strength: 0.6, length: 0.8, mixing: 'pigment' },\n});\n\n/** A solid round blob of paint (one dab). */\nconst PAD_BLOB: PaintBrush = normalizePaintBrush({\n id: 'pad-blob',\n name: 'Pad blob',\n category: 'custom',\n engine: 'dab',\n tip: { size: 20, hardness: 1, spacing: 0.1, opacity: 1, flow: 1, pressureSize: false, pressureOpacity: false },\n});\n\n/** One paint in a `mix`: a colour and its parts. */\nexport interface PadMixPart {\n color: string;\n parts: number;\n}\n\nexport interface PadMixOptions {\n /** Centre of the pile in pad pixels (default the pad's centre). */\n x?: number;\n y?: number;\n /** Radius of the pile in pad pixels, 12..100 (default 40). */\n radius?: number;\n /** Knife passes over the pile, 1..20 (default 3): more passes, finer streaks around the mixed core. */\n passes?: number;\n /** Wipe the pile's area first (default true); false mixes into what is already there. */\n clear?: boolean;\n}\n\nexport class MixingPad {\n readonly width = MIXING_PAD_WIDTH;\n readonly height = MIXING_PAD_HEIGHT;\n private surface: TiledSurface = emptySurface(4);\n /** Increments on every change (for redrawing). */\n version = 0;\n\n /** Paints (`paint`, with `color`) or smears (`knife`) through `points` in pad pixels. */\n stroke(points: readonly PenPoint[], options: { tool: MixingPadTool; color: string; size?: number }): { bounds: { x: number; y: number; width: number; height: number } | null } {\n if (!points.length) throw new Error('points is empty; give at least one {x, y} in pad pixels.');\n for (const [i, p] of points.entries()) {\n if (!Number.isFinite(p.x) || !Number.isFinite(p.y) || p.x < 0 || p.y < 0 || p.x > this.width || p.y > this.height) {\n throw new Error(`points[${i}] (${p.x}, ${p.y}) is off the ${this.width}×${this.height} mixing pad.`);\n }\n }\n let brush = options.tool === 'knife' ? PAD_KNIFE : PAD_PAINT;\n if (options.size !== undefined) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);\n return this.run(brush, points, options.color);\n }\n\n /** Paints `points` with `brush` in `color` on the pad. */\n private run(brush: PaintBrush, points: readonly PenPoint[], color: string): { bounds: { x: number; y: number; width: number; height: number } | null } {\n const [r, g, b] = hexToRgb8(color);\n let result: TiledSurface | null = null;\n const session = new PaintStrokeSession(\n { update: () => {}, commit: (_s, s) => (result = s) },\n {\n layerId: 'mixing-pad',\n surface: this.surface,\n offset: { x: 0, y: 0 },\n lockTransparency: false,\n brush,\n color: [r, g, b, 255],\n background: [255, 255, 255, 255],\n grain: null,\n paperDepth: 0,\n seed: 1,\n colorHex: color,\n affineCopies: [],\n copies: [IDENTITY_COPY],\n mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),\n }\n );\n session.push(points);\n const res = session.end();\n if (result) {\n this.surface = result;\n this.version++;\n }\n return { bounds: res.bounds };\n }\n\n /** The average colour within `radius` pixels of (x, y), or `null` where the pad is bare. */\n pick(x: number, y: number, radius = 3): string | null {\n if (!(x >= 0 && y >= 0 && x < this.width && y < this.height)) throw new Error(`(${x}, ${y}) is off the ${this.width}×${this.height} mixing pad.`);\n const r = Math.max(0, Math.round(radius));\n const x0 = Math.max(0, Math.floor(x) - r);\n const y0 = Math.max(0, Math.floor(y) - r);\n const x1 = Math.min(this.width, Math.floor(x) + r + 1);\n const y1 = Math.min(this.height, Math.floor(y) + r + 1);\n const px = readRegion(this.surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });\n let R = 0;\n let G = 0;\n let B = 0;\n let A = 0;\n for (let i = 0; i < px.length; i += 4) {\n const a = px[i + 3];\n R += px[i] * a;\n G += px[i + 1] * a;\n B += px[i + 2] * a;\n A += a;\n }\n if (A === 0) return null;\n const hex = (v: number) => Math.round(v / A).toString(16).padStart(2, '0');\n return `#${hex(R)}${hex(G)}${hex(B)}`;\n }\n\n clear(): void {\n this.surface = emptySurface(4);\n this.version++;\n }\n\n /**\n * Mixes paints the way a painter does on a palette: blobs whose areas are in proportion to\n * their parts, side by side around (x, y), spiral knife passes that fold them together, and then the centre worked until it is fully mixed: a soft-edged core\n * of the Kubelka-Munk mix of the parts (`mixPigments`), with partly mixed streaks around it.\n * Returns that colour (picked back from the centre) and the pile's bounds.\n *\n * The core is laid, not smeared, because smearing alone drifts: the knife samples in RGB and a\n * mixed colour's scattering is re-estimated from its luminance, so repeated small mixes favour\n * the lighter paint (see GOTCHAS).\n */\n mix(paints: readonly PadMixPart[], options: PadMixOptions = {}): { color: string | null; bounds: { x: number; y: number; width: number; height: number } } {\n if (!paints.length || paints.length > 8) throw new Error(`Give 1 to 8 paints to mix; got ${paints.length}.`);\n const total = paints.reduce((n, p) => n + p.parts, 0);\n paints.forEach((p, i) => {\n if (!(Number.isFinite(p.parts) && p.parts >= 0)) throw new Error(`paints[${i}].parts is ${p.parts}; use a number ≥ 0.`);\n });\n if (!(total > 0)) throw new Error('Every paint has 0 parts; give at least one paint parts above 0.');\n const R = options.radius ?? 40;\n if (!(R >= 12 && R <= 100)) throw new Error(`radius ${R} is out of range; use 12..100 pad px.`);\n const passes = options.passes ?? 3;\n if (!(Number.isInteger(passes) && passes >= 1 && passes <= 20)) throw new Error(`passes ${passes} is out of range; use a whole number 1..20.`);\n const cx = options.x ?? this.width / 2;\n const cy = options.y ?? this.height / 2;\n if (!(cx - R >= 0 && cy - R >= 0 && cx + R <= this.width && cy + R <= this.height)) {\n throw new Error(`A pile of radius ${R} at (${cx}, ${cy}) does not fit on the ${this.width}×${this.height} pad; move it at least ${R} px from every edge or use a smaller radius.`);\n }\n const bounds = { x: Math.floor(cx - R), y: Math.floor(cy - R), width: Math.ceil(2 * R), height: Math.ceil(2 * R) };\n if (options.clear !== false) this.surface = writeRegion(this.surface, bounds, new Uint8ClampedArray(bounds.width * bounds.height * 4));\n // Blobs: equal-area share of a disc of radius 0.7·R per part, placed around the centre.\n const used = paints.filter((p) => p.parts > 0);\n const area = Math.PI * (0.7 * R) ** 2;\n used.forEach((p, i) => {\n const r = Math.sqrt((area * (p.parts / total)) / Math.PI);\n const a = (i / used.length) * Math.PI * 2;\n const off = used.length === 1 ? 0 : 0.45 * R;\n const x = Math.min(cx + R - r, Math.max(cx - R + r, cx + Math.cos(a) * off));\n const y = Math.min(cy + R - r, Math.max(cy - R + r, cy + Math.sin(a) * off));\n const blob = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: Math.max(2, 2 * r) } }, PAD_BLOB);\n this.run(blob, [{ x, y }], p.color.toLowerCase());\n });\n // Knife passes: spiral stirs through the pile, each turned a little, folding the blobs together.\n const knife = normalizePaintBrush({ tip: { ...PAD_KNIFE.tip, size: Math.max(12, R * 0.5) } }, PAD_KNIFE);\n for (let k = 0; k < passes; k++) {\n const stir: PenPoint[] = [];\n const turns = 1.6;\n for (let t = 0; t <= 48; t++) {\n const a = (t / 48) * Math.PI * 2 * turns + k * 1.3;\n const rr = R * (0.85 - 0.6 * (t / 48));\n stir.push({ x: cx + Math.cos(a) * rr, y: cy + Math.sin(a) * rr });\n }\n this.run(knife, stir, '#000000');\n }\n const mixed = mixPigments(\n paints.map((p) => hexToRgb8(p.color).map((v) => v / 255) as [number, number, number]),\n paints.map((p) => p.parts)\n );\n const core = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: R * 1.1, hardness: 0.55 } }, PAD_BLOB);\n const hex = '#' + mixed.map((v) => Math.round(v * 255).toString(16).padStart(2, '0')).join('');\n this.run(core, [{ x: cx, y: cy }], hex);\n return { color: this.pick(cx, cy, 2), bounds };\n }\n\n /** The pad's pixels as a PNG data URL (with alpha), for saving. */\n toPng(): string {\n return pngDataUrl(this.width, this.height, readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height }));\n }\n\n /** Replaces the pad with a PNG (data URL or base64) of the pad's size, as `toPng` writes. */\n fromPng(png: string): void {\n const bytes = png.startsWith('data:') ? parseDataUrl(png)?.bytes : base64ToBytes(png);\n if (!bytes || !isPng(bytes)) throw new Error('The pad image is not a PNG data URL or base64 PNG.');\n const img = decodePng(bytes);\n if (img.width !== this.width || img.height !== this.height) throw new Error(`The pad image is ${img.width}×${img.height}; a mixing pad is ${this.width}×${this.height}.`);\n this.surface = surfaceFromPixels({ width: img.width, height: img.height, data: img.data }, 4);\n this.version++;\n }\n\n /** Whether nothing is on the pad. */\n get empty(): boolean {\n return this.surface.tiles.size === 0;\n }\n\n /** The pad over white, `width × height` RGBA. */\n pixels(): Uint8ClampedArray {\n const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });\n for (let i = 0; i < px.length; i += 4) {\n const a = px[i + 3] / 255;\n px[i] = Math.round(px[i] * a + 255 * (1 - a));\n px[i + 1] = Math.round(px[i + 1] * a + 255 * (1 - a));\n px[i + 2] = Math.round(px[i + 2] * a + 255 * (1 - a));\n px[i + 3] = 255;\n }\n return px;\n }\n}\n","/**\n * Replaying a painting from its data: every recorded stroke, and the changes recorded between them\n * (fills, clears, flattened relief, and wet-media events at the simulation clock they happened at),\n * repainted into a fresh, headless editor.\n *\n * - **Time-lapse**: frames of the painting as it is repainted (after strokes, and while wet paint\n * flows between them), as PNGs and as a `VideoProject` of stills that the video editor's export\n * path (`toVideoFfmpeg`) turns into an MP4.\n * - **High-resolution export**: the same replay at a larger scale (points, brush sizes, paper grain\n * and rectangles scaled), so the picture is painted again with more pixels instead of upscaled.\n *\n * Strokes paint exactly as recorded: same seed, load and symmetry copies, and wet strokes apply\n * each point at the clock it was recorded at, with the simulation stepped in between, so wet paint\n * flows as it did. What was not recorded as data (imported pixels, merges, transforms of layers,\n * changes made before strokes were recorded) is not repeated: those layers start empty.\n */\n\nimport { encodePng } from '../raster/png';\nimport { writeZip } from '../export/zip-writer';\nimport { emptySurface } from '../raster/surface';\nimport { mapLayer, flattenLayers } from '../raster/layers';\nimport type { VideoAsset, VideoClip, VideoProject } from '../video-editor/types';\nimport { toVideoFfmpeg, type VideoFfmpegExport } from '../video-editor/export';\nimport type { PaintDocumentState, PaintLayer, PaintPixelLayer } from './document';\nimport type { PaintEditorController, PixelImage } from './controller';\nimport type { PenPoint } from './engine';\nimport { IDENTITY_AFFINE, varyStroke, type Affine, type RecordedStroke } from './strokes';\nimport type { WetEvent } from './wet';\nimport { scaleStencil } from './stencils';\nimport type { Sticker } from './stickers';\nimport type { WetFlowSpec } from '../paint/wet/flow';\nimport type { WetParams, WetTilt } from '../paint/wet/model';\nimport { downsampleRgba } from './render';\n\nexport interface PaintReplayOptions {\n /** Paint at this many times the size (1 for a time-lapse; 1..8 for a high-resolution export). */\n scale?: number;\n /** Stop after this many strokes (default all). */\n strokes?: number;\n}\n\n/** One frame of a replay. */\nexport interface ReplayFrame extends PixelImage {\n /** Strokes painted so far. */\n stroke: number;\n /** Wet-media clock (steps). */\n clock: number;\n}\n\nexport interface TimeLapseOptions {\n /** Longest side of each frame in pixels (default 720). */\n maxSide?: number;\n /** A frame after every this many strokes (default: about `frames` frames over the whole painting). */\n strokesPerFrame?: number;\n /** About how many frames in all (default 120). */\n frames?: number;\n /** While wet paint flows between strokes, a frame every this many steps (default 60; 0: none). */\n wetStepsPerFrame?: number;\n /** Frames per second of the video (default 12). */\n fps?: number;\n /** Hold the finished painting this long at the end (ms, default 1500). */\n holdMs?: number;\n /** Called after each frame with how far along the replay is (0..1). */\n onProgress?: (done: number) => void;\n}\n\nexport interface TimeLapse {\n fps: number;\n width: number;\n height: number;\n /** Each frame as PNG bytes. */\n frames: Uint8Array[];\n /** The frames as stills on a timeline (assets `frame-0001.png`…; `src` is that file name). */\n video: VideoProject;\n /** The ffmpeg command that renders `video` to `timelapse.mp4` from the frame files. */\n ffmpeg: VideoFfmpegExport;\n durationMs: number;\n}\n\ntype Item = { kind: 'stroke'; stroke: RecordedStroke } | { kind: 'wet'; event: WetEvent } | { kind: 'paint'; event: NonNullable<PaintDocumentState['paintEvents']>[number] };\n\n/** Strokes and events in the order they happened. */\nexport function replayItems(doc: PaintDocumentState): Item[] {\n const items: Array<Item & { at: number; order: number }> = [];\n let order = 0;\n doc.strokes.forEach((stroke, i) => items.push({ kind: 'stroke', stroke, at: i + 0.5, order: order++ }));\n for (const event of doc.paintEvents ?? []) items.push({ kind: 'paint', event, at: event.at, order: order++ });\n for (const event of doc.wetEvents ?? []) items.push({ kind: 'wet', event, at: event.at ?? 0, order: order++ });\n items.sort((a, b) => a.at - b.at || a.order - b.order);\n return items.map(({ at: _a, order: _o, ...item }) => item as Item);\n}\n\n/** The layer tree with every pixel layer empty (what a replay paints into). */\nfunction emptiedLayers(layers: readonly PaintLayer[]): PaintLayer[] {\n let out = [...layers] as PaintLayer[];\n for (const l of flattenLayers(layers)) if (l.type === 'pixel') out = mapLayer(out, l.id, () => ({ ...(l as PaintPixelLayer), surface: emptySurface(4), offset: { x: 0, y: 0 } }));\n return out;\n}\n\n/**\n * Replays a controller's painting into `target` (a fresh controller the caller made at the scaled\n * size; see `PaintEditorController.createReplayTarget`), calling `onFrame` where frames are due.\n */\nexport async function replayPainting(\n source: PaintDocumentState,\n target: PaintEditorController,\n options: PaintReplayOptions & { onStroke?: (index: number) => Promise<void> | void; onWetSteps?: (clock: number) => Promise<void> | void; wetChunk?: number } = {}\n): Promise<void> {\n const s = options.scale ?? 1;\n const scaleM: Affine = [s, 0, 0, s, 0, 0];\n const items = replayItems(source);\n const maxStrokes = options.strokes ?? source.strokes.length;\n const chunk = Math.max(1, options.wetChunk ?? 0) || Infinity;\n let strokes = 0;\n const advanceTo = async (clock: number) => {\n const now = target.getState().wet.clock;\n let left = clock - now;\n while (left > 0) {\n const n = Math.min(left, chunk);\n await target.advanceWet({ steps: n });\n left -= n;\n if (Number.isFinite(chunk)) await options.onWetSteps?.(target.getState().wet.clock);\n }\n };\n const sr = (r: { x: number; y: number; width: number; height: number }) => ({ x: r.x * s, y: r.y * s, width: r.width * s, height: r.height * s });\n for (const item of items) {\n if (strokes >= maxStrokes && item.kind === 'stroke') break;\n if (item.kind === 'paint') {\n const e = item.event;\n if (!target.getLayer(e.layerId)) continue;\n if (e.kind === 'fill') target.fillLayer({ layerId: e.layerId, ...(e.data as object) });\n else if (e.kind === 'clear') target.clearLayer(e.layerId);\n else if (e.kind === 'stickers') {\n const list = ((e.data as { stickers?: Sticker[] })?.stickers ?? []).map((x) => (s === 1 ? x : { ...x, x: x.x * s, y: x.y * s, size: x.size * s }));\n target.replaceStickers(e.layerId, list);\n } else target.flattenRelief(e.layerId);\n continue;\n }\n if (item.kind === 'wet') {\n const e = item.event;\n await target.wetIdle();\n await advanceTo(e.clock);\n try {\n switch (e.kind) {\n case 'dry': {\n const rect = e.data as { x: number; y: number; width: number; height: number } | undefined;\n const ids = e.layerId?.split(',').filter(Boolean);\n if (ids?.length) for (const id of ids) target.dryPaint({ layerId: id, rect: rect ? sr(rect) : undefined });\n else target.dryPaint({ rect: rect ? sr(rect) : undefined });\n break;\n }\n case 'tilt':\n target.setCanvasTilt(e.data as WetTilt);\n break;\n case 'params':\n if (e.layerId && target.getLayer(e.layerId)) target.setWetParams(e.layerId, e.data as WetParams);\n break;\n case 'flow': {\n const f = { ...(e.data as WetFlowSpec) };\n if (f.center) f.center = { x: f.center.x * s, y: f.center.y * s };\n if (f.scale) f.scale = Math.min(1024, f.scale * s);\n if (e.layerId && target.getLayer(e.layerId)) target.setFlowMap(e.layerId, f);\n break;\n }\n case 'wet-paper': {\n const d = e.data as { rect: { x: number; y: number; width: number; height: number }; amount: number };\n if (e.layerId && target.getLayer(e.layerId)) target.wetPaper({ layerId: e.layerId, rect: sr(d.rect), amount: d.amount });\n break;\n }\n }\n } catch {\n // An event that no longer applies (its layer went away) is skipped.\n }\n continue;\n }\n const st = s === 1 ? item.stroke : varyStroke(item.stroke, scaleM, { scale: s });\n strokes++;\n if (!target.getLayer(st.layerId)) continue;\n const init = { layerId: st.layerId, color: st.color, brush: st.brush, seed: st.seed, copies: (st.copies ?? []) as Affine[], ...(st.load ? { load: st.load } : {}), ...(st.paint ? { paint: st.paint } : {}), snapToPerspective: false, stencils: false, maskStencils: (st.stencils ?? []).map((x) => (s === 1 ? x : scaleStencil(x, s))) };\n if (st.brush.engine === 'wet' && st.steps?.length === st.points.length) {\n // Each point at the clock it was recorded at, the simulation stepped in between.\n await target.wetIdle();\n await advanceTo(st.steps[0]);\n const session = target.beginStroke({ ...init, start: st.points[0] });\n for (let i = 0; i < st.points.length; i++) {\n await advanceTo(st.steps[i]);\n await target.wetIdle();\n session.push([st.points[i] as PenPoint]);\n }\n await target.wetIdle();\n session.end();\n while (session.pendingEnd) {\n await target.wetIdle();\n session.completeWetEnd();\n if (session.pendingEnd) await new Promise((r) => setTimeout(r, 0));\n }\n } else {\n await target.wetIdle();\n target.paintStroke({ ...init, points: st.points });\n }\n await options.onStroke?.(strokes);\n }\n void IDENTITY_AFFINE;\n}\n\n/** A frame of `target` at most `maxSide` pixels on its longest side. */\nexport function replayFrame(target: PaintEditorController, maxSide: number, stroke: number): ReplayFrame {\n const img = target.renderPreview({ maxSide, wet: 'display' });\n return { ...img, stroke, clock: target.getState().wet.clock };\n}\n\n/** Frames as stills on a video timeline, `1 / fps` each, with the last held for `holdMs`. */\nexport function timeLapseVideo(frameCount: number, width: number, height: number, fps: number, holdMs: number, name = 'Time-lapse'): { video: VideoProject; ffmpeg: VideoFfmpegExport; durationMs: number } {\n const w = Math.max(2, 2 * Math.round(width / 2));\n const h = Math.max(2, 2 * Math.round(height / 2));\n const frameMs = 1000 / fps;\n const assets: VideoAsset[] = [];\n const clips: VideoClip[] = [];\n for (let i = 0; i < frameCount; i++) {\n const id = `frame-${String(i + 1).padStart(4, '0')}`;\n assets.push({ id, kind: 'image', src: `${id}.png`, name: `${id}.png`, width, height });\n const start = Math.round(i * frameMs * 1000) / 1000;\n const end = i === frameCount - 1 ? start + frameMs + holdMs : Math.round((i + 1) * frameMs * 1000) / 1000;\n clips.push({ id: `clip-${i + 1}`, type: 'media', asset: id, start, duration: end - start } as VideoClip);\n }\n const video: VideoProject = { version: 1, name, width: w, height: h, fps, background: '#000000', assets, tracks: [{ id: 'v1', kind: 'video', clips } as VideoProject['tracks'][number]] };\n const durationMs = frameCount ? clips[clips.length - 1].start + clips[clips.length - 1].duration : 0;\n return { video, ffmpeg: toVideoFfmpeg(video, { output: 'timelapse.mp4' }), durationMs };\n}\n\n/** Builds the time-lapse of a painting (see `PaintEditorController.timeLapse`). */\nexport async function buildTimeLapse(source: PaintDocumentState, target: PaintEditorController, options: TimeLapseOptions = {}): Promise<TimeLapse> {\n const maxSide = Math.max(32, Math.min(4096, Math.round(options.maxSide ?? 720)));\n const total = source.strokes.length;\n const perFrame = Math.max(1, options.strokesPerFrame ?? Math.ceil(total / Math.max(1, options.frames ?? 120)));\n const wetEvery = options.wetStepsPerFrame ?? 60;\n const fps = Math.max(1, Math.min(60, options.fps ?? 12));\n const frames: Uint8Array[] = [];\n let width = 0;\n let height = 0;\n const shoot = (stroke: number) => {\n const f = replayFrame(target, maxSide, stroke);\n width = f.width;\n height = f.height;\n frames.push(encodePng(f.width, f.height, f.data));\n };\n shoot(0);\n await replayPainting(source, target, {\n wetChunk: wetEvery > 0 ? wetEvery : 0,\n onStroke: (i) => {\n if (i % perFrame === 0 || i === total) shoot(i);\n options.onProgress?.(total ? i / total : 1);\n },\n onWetSteps: wetEvery > 0 ? () => shoot(-1) : undefined,\n });\n // Let the last wet paint settle on screen.\n const { video, ffmpeg, durationMs } = timeLapseVideo(frames.length, width, height, fps, options.holdMs ?? 1500);\n return { fps, width, height, frames, video, ffmpeg, durationMs };\n}\n\n/** A contact sheet: up to `count` frames side by side, for a quick look. */\nexport function contactSheet(frames: readonly PixelImage[], columns = 3, cell = 200): PixelImage {\n const n = frames.length;\n const rows = Math.ceil(n / columns);\n if (n) cell = Math.min(cell, frames[0].width);\n const ar = n ? frames[0].height / frames[0].width : 1;\n const ch = Math.max(1, Math.round(cell * ar));\n const out = new Uint8ClampedArray(columns * cell * rows * ch * 4).fill(255);\n const W = columns * cell;\n frames.forEach((f, i) => {\n const small = downsampleRgba(f.data, f.width, f.height, cell, ch);\n const ox = (i % columns) * cell;\n const oy = Math.floor(i / columns) * ch;\n for (let y = 0; y < ch; y++) out.set(small.subarray(y * cell * 4, (y + 1) * cell * 4), ((oy + y) * W + ox) * 4);\n });\n return { width: W, height: rows * ch, data: out };\n}\n\nexport { emptiedLayers };\n\n/**\n * A time-lapse as one ZIP: the frames (`frame-0001.png`…), the video project (`timelapse.json`,\n * which the video editor opens), and `render.sh` with the ffmpeg command that turns them into\n * `timelapse.mp4`.\n */\nexport async function timeLapseZip(tl: TimeLapse): Promise<Uint8Array> {\n const entries = tl.frames.map((data, i) => ({ name: `frame-${String(i + 1).padStart(4, '0')}.png`, data }));\n entries.push({ name: 'timelapse.json', data: new TextEncoder().encode(JSON.stringify(tl.video, null, 2)) });\n entries.push({ name: 'render.sh', data: new TextEncoder().encode(`#!/bin/sh\\n# Renders the time-lapse with ffmpeg (run in this folder).\\n${tl.ffmpeg.command}\\n`) });\n return writeZip(entries, { compression: 'store' });\n}\n","/**\n * `PaintEditorController`: the natural-media paint editor's state, framework-free.\n *\n * - The document (canvas, paper, the shared raster layer stack, recorded strokes) is immutable\n * and tiled copy-on-write; each undo step is a reference to a whole document state.\n * - Strokes: `beginStroke` gives a live session for pointer input (one undo step at `end`);\n * `paintStroke` paints a whole stroke from data (agents, tests, replays).\n * - The brush, colour, Mixer Brush load, view and hand are editor state, not undo steps.\n * - Rendering goes through the shared compositor with the paper as its bottom layer; exports\n * are PNG (pure TypeScript), JPEG / WebP (through a canvas or a host encoder) and PSD (through\n * the shared PSD writer, with the paper as a pixel layer).\n */\n\nimport { exportPsd, type PsdExportResult } from '../image-editor/psd/export';\nimport type { ImageLayer } from '../image-editor/layers/model';\nimport { applyStrokeToTile, CoverageMap, type StrokeApply } from '../paint/brush';\nimport { cleanMixer, EMPTY_MIXER_LOAD, loadMixer, type MixerLoad } from '../paint/mixer';\nimport { paintSourceColor, parsePaintSource, PaintSourceError, resolvePaintGradient, type PaintGradientSource, type PaintSource } from '../paint/gradient-paint';\nimport { BLEND_MODES, isBlendMode, standardBlendMode, type BlendMode } from '../raster/blend';\nimport { findLayer, flattenLayers, insertLayer, isLayerLocked, mapLayer, type LayerLocks } from '../raster/layers';\nimport { bytesToBase64, decodePng, encodePng, parseDataUrl, pngDataUrl } from '../raster/png';\nimport {\n translateSurface,\n contentBounds,\n emptySurface,\n readPixel,\n surfaceFromPixels,\n TILE,\n TiledSurface,\n tileKey,\n tilesInRect,\n finishTile,\n type RasterRect,\n type Tile,\n} from '../raster/surface';\nimport { brushToJSON, BUILTIN_BRUSHES, DEFAULT_BRUSH_ID, normalizePaintBrush, type PaintBrushInput, parseBrushLibrary, BRUSH_ID, type BrushLibraryFile, type PaintBrush } from './brushes';\nimport {\n createPaintDocument,\n createPaintLayer,\n newPaintLayerId,\n normalizeCanvas,\n type PaintCanvas,\n type PaintDocumentState,\n type PaintLayer,\n type PaintPixelLayer,\n type PaintEvent,\n} from './document';\nimport { defaultImageEncoder, type ImageEncoder, type ImageFormat } from './encode';\nimport type { PenPoint } from './engine';\nimport type { Hand } from './input';\nimport { mix32 } from './noise';\nimport { clonePaper, grainCatch, hexToRgb8, normalizePaper, paperPreset, type PaperSettings } from './paper';\nimport { createPaintCompositor, downsampleRgba, paperGrain, paperNode, TRACING_NODE_ID, type ImageNode, type ViewLayer } from './render';\nimport {\n deserializePaintDocument,\n deserializePaintDocumentAsync,\n serializePaintDocument,\n serializePaintDocumentExternal,\n type PaintAssetStore,\n type PaintDocumentExtensions,\n type SerializedPaintDocument,\n} from './serialize';\nimport { brushLoadForStroke, PaintStrokeSession, type PaintStrokeResult, type StrokeSessionHost } from './stroke';\nimport { affineCopy, strokeBounds, variationAffine, varyStroke, IDENTITY_AFFINE, type Affine, type RecordedStroke, type StrokeVariation } from './strokes';\nimport { renderBrushThumbnail, type BrushThumbnail } from './thumbnails';\nimport { colorHarmony, DEFAULT_SWATCHES, readSwatch, type ColorWheel, type HarmonyColor, type HarmonyKind, type Swatch } from './color';\nimport {\n defaultGuides,\n normalizePerspective,\n normalizeSymmetry,\n PerspectiveSnapper,\n symmetryCopies,\n type PaintGuides,\n type PerspectiveGuide,\n type ReferenceImage,\n type SymmetryGuide,\n type TracingImage,\n} from './guides';\nimport { MixingPad, type MixingPadTool, type PadMixOptions } from './mixing-pad';\nimport { normalizeStencil, StencilSnapper, stencilMask, type Stencil, type StencilInput } from './stencils';\nimport { normalizeStickerUpdate, STAMP_KINDS, stickerSurface, type Sticker } from './stickers';\nimport { DEFAULT_VIEW, normalizeView, type PaintView } from './view';\nimport { brushTakesPaint } from './brushes';\nimport { PaintWetLayers, WET_GPU_MODES, WetStrokeDriver, type PaintWetOptions, type WetEngineInfo, type WetEvent, type WetGpuMode, type WetLayerInfo, type WetLayerSettings } from './wet';\nimport { isWetBrush } from './brushes';\nimport { normalizeWetParams, normalizeTilt, type WetParams, type WetTilt } from '../paint/wet/model';\nimport { normalizeFlow, type WetFlowSpec } from '../paint/wet/flow';\nimport { MAX_WET_LANES, type WetBackendInfo, type WetSnapshot } from '../paint/wet/world';\nimport { wetSupport } from '../paint/wet/wasm';\nimport { buildTimeLapse, emptiedLayers, replayPainting, type TimeLapse, type TimeLapseOptions } from './replay';\nimport { DEFAULT_LIGHTING, emptyThickChannels, LitCache, normalizeLighting, reliefInfo, thickFromJSON, thickToJSON, type PaintLighting, type ThickChannels } from './thick';\n\n/** A refusal with a message that says what is wrong and how to fix it. */\nexport class PaintEditorError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PaintEditorError';\n }\n}\n\nexport interface PaintEditorOptions extends Partial<PaintCanvas> {\n /** Paper settings, or a library preset id (default cold pressed). */\n paper?: Partial<PaperSettings> | string;\n /** Active brush id (default `pencil-6b`). */\n brushId?: string;\n /** Saved user brushes to start the library with. */\n brushes?: readonly PaintBrush[];\n /** Paint colour #rrggbb (default #1f2937). */\n color?: string;\n hand?: Hand;\n /** Encodes JPEG / WebP (default: a canvas, when there is one). */\n encodeImage?: ImageEncoder;\n /** Undo steps kept (default 200). */\n historyLimit?: number;\n /** Wet media: kernel, threads, speed (steps per second) and the memory kept for undoing wet strokes. */\n wet?: PaintWetOptions;\n}\n\n/** Wet media as panels show it. */\nexport interface PaintWetStateInfo {\n /** Layers with a wet layer (paint still wet or drying). */\n layerIds: string[];\n /** Some paint is wet. */\n wet: boolean;\n /** Time is stopped (the paint stays wet). */\n paused: boolean;\n /** The water overlay is shown. */\n overlay: boolean;\n /** Wet strokes dry as soon as they end. */\n instantDry: boolean;\n /** Steps per second while time runs. */\n speed: number;\n /** Simulation steps so far. */\n clock: number;\n tilt: WetTilt;\n /** Engine B (WebGPU): `auto` (used for large wet areas when there is a GPU) or `off` (CPU only). */\n gpu: WetGpuMode;\n /** Engine B is shading the wet paint now. */\n gpuActive: boolean;\n}\n\nexport interface PaintEditorState {\n document: PaintDocumentState;\n /** The active brush (possibly edited from its preset). */\n brush: PaintBrush;\n /** The brush library: built-in presets, then the user's brushes. */\n brushes: readonly PaintBrush[];\n /** Paint colour #rrggbb. */\n color: string;\n /** The second colour (swap with `swapColors`). */\n secondaryColor: string;\n /**\n * What strokes paint with: the paint colour, or gradient paint (`setPaint`). A brush with its\n * own gradient (`brush.paint`) paints that instead.\n */\n paint: PaintSource;\n /** What the Mixer Brush carries between strokes. */\n mixerLoad: MixerLoad;\n view: PaintView;\n hand: Hand;\n canUndo: boolean;\n canRedo: boolean;\n undoLabel: string | null;\n redoLabel: string | null;\n /** Mirror / kaleidoscope, perspective, tracing image and reference images (saved with the document, not undo steps). */\n guides: PaintGuides;\n /** The colour swatches (saved with the document, not undo steps). */\n swatches: readonly Swatch[];\n /** Changes whenever the mixing pad does. */\n mixingPadVersion: number;\n /** Mixing pads kept by name (saved with the document). */\n savedMixingPads: readonly SavedMixingPadInfo[];\n /** A stroke is in progress. */\n stroking: boolean;\n /** Wet media. */\n wet: PaintWetStateInfo;\n /** Increments on every change. */\n version: number;\n}\n\n/** A layer as tools and panels describe it. */\nexport interface PaintLayerInfo {\n id: string;\n name: string;\n type: 'pixel' | 'group';\n visible: boolean;\n opacity: number;\n blend: string;\n clipped: boolean;\n locks: LayerLocks;\n active: boolean;\n /** Painted content (document pixels), or `null` when empty. */\n bounds: RasterRect | null;\n parentId: string | null;\n depth: number;\n}\n\nexport interface PaintStrokeInit {\n /** Default the active layer. */\n layerId?: string;\n /** Default the editor colour. */\n color?: string;\n /**\n * What this stroke paints with, instead of the editor's paint: a colour, or gradient paint\n * (`@pixerate/editor/paint` `PaintSource`). A colour here is the same as `color`.\n */\n paint?: PaintSource | string;\n /** Default the active brush. */\n brushId?: string;\n /** Changes to the brush for this stroke only. */\n brush?: PaintBrushInput;\n /** Jitter seed (default: a new one per stroke). */\n seed?: number;\n /** Extra copies of the stroke as affine maps (default: the symmetry guide's). */\n copies?: readonly Affine[];\n /** Snap the samples to the perspective guide (default true for `beginStroke`, false for `paintStroke`). */\n snapToPerspective?: boolean;\n /** Where the stroke starts, for taking its colour from the tracing image before the first sample (`beginStroke`). */\n start?: { x: number; y: number };\n /** What the brush carries at the start (default: from the editor's brush load); replays pass the recorded one. */\n load?: MixerLoad;\n /** Follow guide stencils and keep to mask stencils (default true; replays pass false, their points already follow them). */\n stencils?: boolean;\n /** Mask with these stencils instead (replays pass the ones recorded with the stroke). */\n maskStencils?: readonly Stencil[];\n}\n\nexport interface LayerUpdate {\n name?: string;\n visible?: boolean;\n opacity?: number;\n blend?: BlendMode;\n clipped?: boolean;\n locks?: LayerLocks;\n}\n\nexport interface FillLayerOptions {\n layerId?: string;\n /** Default the editor colour. */\n color?: string;\n /** 0..1 (default 1). */\n opacity?: number;\n /** Paper grain response 0..1 (default 0: a flat fill); above 0 the fill catches the grain like pastel rubbed across the paper. */\n grain?: number;\n /** With grain: pressure 0..1, how far the fill reaches into the valleys (default 0.5). */\n pressure?: number;\n blend?: BlendMode;\n}\n\nexport interface ReplayOptions extends StrokeVariation {\n /** Recorded stroke ids (see `getStrokes`). */\n strokeIds: readonly string[];\n /** Paint on this layer instead of each stroke's own. */\n layerId?: string;\n /** Recolour (default each stroke's colour). */\n color?: string;\n /** Replay with this brush instead (default each stroke's brush). */\n brushId?: string;\n /** Repeat the variation this many times, each one applied on top of the last (default 1). */\n repeat?: number;\n}\n\nexport interface RenderOptions {\n /** Include the paper (default true); without it the result is transparent where nothing is painted. */\n paper?: boolean;\n /** Only these layers (a group with its children). */\n layerIds?: readonly string[];\n /**\n * Wet paint as it is now (`display`: still wet, wet paper a little darker) or dried where it is\n * (`dried`, the deterministic bake that saving uses). Default `dried` for exports and `display`\n * for previews and picks.\n */\n wet?: 'display' | 'dried';\n}\n\nexport interface PixelImage {\n width: number;\n height: number;\n data: Uint8ClampedArray;\n}\n\n/** A mixing pad kept by name. */\nexport interface SavedMixingPadInfo {\n id: string;\n name: string;\n}\n\ninterface SavedMixingPad extends SavedMixingPadInfo {\n /** The pad as a PNG data URL. */\n png: string;\n}\n\ninterface HistoryEntry {\n label: string;\n before: PaintDocumentState;\n after: PaintDocumentState;\n /** Wet steps: the wet simulation before and after (`lost` once dropped for memory: undo then leaves the paint dry). */\n wet?: { before: WetSnapshot | null; after: WetSnapshot | null; lost: boolean };\n}\n\nconst HEX = /^#[0-9a-f]{6}$/i;\n\nfunction parseColor(value: unknown, what = 'color'): string {\n if (typeof value !== 'string' || !HEX.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. \"#2a4b8d\".`);\n return value.trim().toLowerCase();\n}\n\nfunction rgbaOf(hex: string): [number, number, number, number] {\n const [r, g, b] = hexToRgb8(hex);\n return [r, g, b, 255];\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n return '#' + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('');\n}\n\nfunction unionRect(a: RasterRect | null, b: RasterRect | null): RasterRect | null {\n if (!a) return b ? { ...b } : null;\n if (!b) return { ...a };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\nconst isPixelLayer = (l: PaintLayer | undefined): l is PaintPixelLayer => l?.type === 'pixel';\n\nexport class PaintEditorController {\n private doc: PaintDocumentState;\n private extensions: PaintDocumentExtensions = {};\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private readonly historyLimit: number;\n private brush: PaintBrush;\n private userBrushes: PaintBrush[];\n /** The library list in the state (the same array until the user brushes change, so thumbnails are not redrawn). */\n private library: readonly PaintBrush[] | null = null;\n private color: string;\n private secondaryColor = '#ffffff';\n /** Gradient paint chosen with `setPaint` (`null`: the paint colour). */\n private gradient: PaintGradientSource | null = null;\n private mixerLoad: MixerLoad = EMPTY_MIXER_LOAD;\n private view: PaintView = { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };\n private hand: Hand;\n private readonly encodeImage: ImageEncoder | undefined;\n private listeners = new Set<() => void>();\n private version = 0;\n private cached: PaintEditorState | null = null;\n private session: PaintStrokeSession | null = null;\n private live: { layerId: string; surface: TiledSurface } | null = null;\n private liveLayers: { key: TiledSurface; layers: PaintLayer[] } | null = null;\n private dirty: RasterRect[] | 'all' = 'all';\n private seedCounter = 1;\n private strokeCounter = 1;\n private guides: PaintGuides;\n private swatches: Swatch[] = DEFAULT_SWATCHES.map((s) => ({ ...s }));\n private readonly pad = new MixingPad();\n private savedPads: SavedMixingPad[] = [];\n private nextPadId = 1;\n private tracingVersion = 1;\n private tracingCache: { key: number; node: ImageNode } | null = null;\n private readonly imagePixels = new Map<string, PixelImage>();\n private nextReferenceId = 1;\n private readonly viewCompositor = createPaintCompositor();\n private readonly exportCompositor = createPaintCompositor();\n /** Wet media: the simulation, its settings and time. */\n readonly wet: PaintWetLayers;\n private readonly wetOptions: PaintWetOptions | undefined;\n /** Where the light comes from for thick paint (saved with the document, not undo steps). */\n private lighting: PaintLighting = { ...DEFAULT_LIGHTING };\n private readonly litCaches = new Map<string, LitCache>();\n /** The live impasto stroke's channels. */\n private liveThick: { layerId: string; channels: ThickChannels } | null = null;\n /** Set while an operation that changes wet paint runs: the simulation before it (for undo). */\n private wetOp: { before: WetSnapshot | null } | null = null;\n\n constructor(options: PaintEditorOptions = {}) {\n this.historyLimit = Math.max(1, options.historyLimit ?? 200);\n this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });\n this.userBrushes = (options.brushes ?? []).map((b) => normalizePaintBrush({ ...b, builtin: false }));\n const start = this.findBrush(options.brushId ?? DEFAULT_BRUSH_ID) ?? this.findBrush(DEFAULT_BRUSH_ID)!;\n this.brush = normalizePaintBrush(start);\n this.color = options.color ? parseColor(options.color) : '#1f2937';\n this.hand = options.hand === 'left' ? 'left' : 'right';\n this.encodeImage = options.encodeImage;\n this.guides = defaultGuides(this.doc.canvas);\n this.wetOptions = options.wet;\n this.wet = new PaintWetLayers(\n {\n canvas: () => this.doc.canvas,\n paper: () => this.doc.paper,\n layerSurface: (id) => {\n const l = findLayer(this.doc.layers, id)?.layer;\n return isPixelLayer(l) ? l.surface : null;\n },\n updateSurfaces: (surfaces, rects) => {\n let layers = this.doc.layers;\n for (const [id, surface] of surfaces) if (findLayer(layers, id)) layers = mapLayer(layers, id, (l) => ({ ...(l as PaintPixelLayer), surface }));\n // Wet paint moving is not an undo step: keep the history's documents.\n this.doc = { ...this.doc, layers };\n this.emit(rects.length ? rects : 'all');\n },\n changed: () => {},\n },\n options.wet\n );\n this.wet.onDriverDone = () => this.session?.completeWetEnd();\n }\n\n // --- State and events ----------------------------------------------------------\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private emit(dirty: RasterRect[] | 'all' = 'all'): void {\n if (dirty === 'all' || this.dirty === 'all') this.dirty = 'all';\n else this.dirty.push(...dirty);\n this.version++;\n this.cached = null;\n for (const l of [...this.listeners]) l();\n }\n\n /** A snapshot of the editor (the same object until something changes). */\n getState(): PaintEditorState {\n if (this.cached) return this.cached;\n const last = this.undoStack[this.undoStack.length - 1];\n const next = this.redoStack[this.redoStack.length - 1];\n this.cached = {\n document: this.doc,\n brush: this.brush,\n brushes: (this.library ??= [...BUILTIN_BRUSHES, ...this.userBrushes]),\n color: this.color,\n secondaryColor: this.secondaryColor,\n paint: this.gradient ?? { type: 'color', color: this.color },\n mixerLoad: this.mixerLoad,\n view: this.view,\n hand: this.hand,\n canUndo: this.undoStack.length > 0 && !this.session,\n canRedo: this.redoStack.length > 0 && !this.session,\n undoLabel: last?.label ?? null,\n redoLabel: next?.label ?? null,\n guides: this.guides,\n swatches: this.swatches,\n mixingPadVersion: this.pad.version,\n savedMixingPads: this.savedPads.map(({ id, name }) => ({ id, name })),\n stroking: Boolean(this.session),\n wet: {\n layerIds: this.wet.wetLayerIds(),\n wet: this.wet.isWet(),\n paused: this.wet.paused,\n overlay: this.wet.overlay,\n instantDry: this.wet.instantDry,\n speed: this.wet.speed,\n clock: this.wet.world?.clock ?? 0,\n tilt: { ...this.wet.tilt },\n gpu: this.wet.gpuMode,\n gpuActive: Boolean(this.wet.world?.gpuActive),\n },\n version: this.version,\n };\n return this.cached;\n }\n\n get document(): PaintDocumentState {\n return this.doc;\n }\n\n /**\n * Where the rendered stack changed since the last call: rectangles (a stroke's dabs) or `'all'`.\n * The viewport passes it to `compositeTiles` as `dirty`.\n */\n takeDirty(): RasterRect[] | 'all' {\n const d = this.dirty;\n this.dirty = [];\n return d;\n }\n\n // --- History -------------------------------------------------------------------\n\n private requireIdle(what: string): void {\n if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);\n }\n\n private commit(label: string, next: PaintDocumentState, before = this.doc): void {\n const entry: HistoryEntry = { label, before, after: next };\n if (this.wetOp) {\n entry.wet = { before: this.wetOp.before, after: this.wet.snapshot(), lost: false };\n this.wetOp = null;\n }\n this.undoStack.push(entry);\n if (this.undoStack.length > this.historyLimit) this.undoStack.shift();\n this.redoStack = [];\n this.doc = next;\n this.trimWetHistory();\n this.emit();\n }\n\n /** Keeps the wet snapshots in history within the wet undo memory, dropping the oldest. */\n private trimWetHistory(): void {\n const seen = new Set<WetSnapshot>();\n let bytes = 0;\n // The nearest steps first: the top of the undo stack, then the redo stack.\n const order = [...[...this.undoStack].reverse(), ...[...this.redoStack].reverse()];\n for (const e of order) {\n if (!e.wet || e.wet.lost) continue;\n for (const snap of [e.wet.before, e.wet.after]) if (snap && !seen.has(snap)) {\n seen.add(snap);\n bytes += snap.bytes;\n }\n if (bytes > this.wet.undoMemory) e.wet = { before: null, after: null, lost: true };\n }\n }\n\n /** Throws when wet paint is mid-step (a batch in worker threads); `wetIdle()` waits for it. */\n private requireWetIdle(what: string): void {\n if (this.wet.busy) throw new PaintEditorError(`Wet paint is mid-step; wait a moment (await wetIdle()) before ${what}.`);\n }\n\n /** Starts an operation that changes wet paint: keeps the simulation as it is now for undo. */\n private beginWetOp(): void {\n this.requireWetIdle('changing wet paint');\n if (!this.wetOp) this.wetOp = { before: this.wet.snapshot() };\n }\n\n /**\n * Before a non-wet change to layers that hold wet paint (a dry brush, a fill, a merge…): dries\n * them first, in the same undo step. Returns the ids dried.\n */\n private dryLayersFirst(ids: readonly string[]): string[] {\n const wetIds = ids.filter((id) => this.wet.wetLayerIds().includes(id));\n if (!wetIds.length) return [];\n this.beginWetOp();\n for (const id of wetIds) this.wet.dry(id);\n this.logWet({ kind: 'dry', layerId: wetIds.join(',') });\n return wetIds;\n }\n\n /** Records a wet event (for replays) in the document being built. */\n private pendingWetEvents: WetEvent[] = [];\n\n private logWet(e: Omit<WetEvent, 'clock'>): void {\n this.pendingWetEvents.push({ clock: this.wet.world?.clock ?? 0, at: this.doc.strokes.length, ...e });\n }\n\n /** The document with a paint event appended (for replays). */\n private withPaintEvent(doc: PaintDocumentState, e: Omit<PaintEvent, 'at'>): PaintDocumentState {\n return { ...doc, paintEvents: [...(doc.paintEvents ?? []), { at: doc.strokes.length, ...e }] };\n }\n\n /** The document with the pending wet events appended. */\n private withWetEvents(doc: PaintDocumentState): PaintDocumentState {\n if (!this.pendingWetEvents.length) return doc;\n const events = [...(doc.wetEvents ?? []), ...this.pendingWetEvents];\n this.pendingWetEvents = [];\n return { ...doc, wetEvents: events };\n }\n\n undo(): string | null {\n this.requireIdle('undoing');\n const e = this.undoStack[this.undoStack.length - 1];\n if (!e) return null;\n if (e.wet) this.requireWetIdle('undoing');\n this.undoStack.pop();\n this.redoStack.push(e);\n const paper = this.doc.paper;\n this.doc = e.before;\n if (e.wet) {\n if (paper !== this.doc.paper) this.wet.paperChanged();\n this.wet.restore(e.wet.lost ? null : e.wet.before);\n if (paper !== this.doc.paper) this.wet.paperChanged();\n }\n this.emit();\n return e.label;\n }\n\n redo(): string | null {\n this.requireIdle('redoing');\n const e = this.redoStack[this.redoStack.length - 1];\n if (!e) return null;\n if (e.wet) this.requireWetIdle('redoing');\n this.redoStack.pop();\n this.undoStack.push(e);\n const paper = this.doc.paper;\n this.doc = e.after;\n if (e.wet) {\n this.wet.restore(e.wet.lost ? null : e.wet.after);\n if (paper !== this.doc.paper) this.wet.paperChanged();\n }\n this.emit();\n return e.label;\n }\n\n /** Labels of the undo steps, oldest first. */\n historyLabels(): { undo: string[]; redo: string[] } {\n return { undo: this.undoStack.map((e) => e.label), redo: this.redoStack.map((e) => e.label).reverse() };\n }\n\n // --- Canvas and paper ----------------------------------------------------------\n\n private resolvePaper(paper: Partial<PaperSettings> | string | undefined): Partial<PaperSettings> {\n if (typeof paper === 'string') {\n const p = paperPreset(paper);\n if (!p) throw new PaintEditorError(`Unknown paper \"${paper}\". Call list_papers for the ids.`);\n return p;\n }\n return paper ?? {};\n }\n\n /** Starts a new canvas: one empty layer over the paper (one undo step; undo brings the old one back). */\n newCanvas(options: Partial<PaintCanvas> & { paper?: Partial<PaperSettings> | string } = {}): PaintDocumentState {\n this.requireIdle('starting a new canvas');\n const next = createPaintDocument({\n width: options.width ?? this.doc.canvas.width,\n height: options.height ?? this.doc.canvas.height,\n dpi: options.dpi ?? this.doc.canvas.dpi,\n paper: options.paper === undefined ? this.doc.paper : this.resolvePaper(options.paper),\n });\n this.extensions = {};\n this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };\n this.tracingVersion++;\n if (this.wet.world) {\n this.beginWetOp();\n this.wet.reset();\n }\n this.commit('New canvas', next);\n return next;\n }\n\n /** Changes the canvas size (keeping the pixels where they are, anchored top-left) or resolution. */\n setCanvas(update: Partial<PaintCanvas>): PaintCanvas {\n this.requireIdle('changing the canvas');\n const canvas = normalizeCanvas({\n width: update.width ?? this.doc.canvas.width,\n height: update.height ?? this.doc.canvas.height,\n dpi: update.dpi ?? this.doc.canvas.dpi,\n });\n if (this.wet.world && (canvas.width !== this.doc.canvas.width || canvas.height !== this.doc.canvas.height)) {\n // The wet simulation is canvas-sized: the paint dries first.\n this.dryLayersFirst(this.wet.wetLayerIds());\n this.wet.reset();\n }\n this.commit('Canvas size', this.withWetEvents({ ...this.doc, canvas }));\n return canvas;\n }\n\n /** Changes the paper: a library preset (`preset`), and / or any of its settings (one undo step). */\n setPaper(update: Partial<PaperSettings> & { preset?: string }): PaperSettings {\n this.requireIdle('changing the paper');\n let base = this.doc.paper;\n if (update.preset !== undefined && update.preset !== this.doc.paper.preset) {\n const p = paperPreset(update.preset);\n if (!p) throw new PaintEditorError(`Unknown paper \"${update.preset}\". Call list_papers for the ids.`);\n base = p;\n }\n const { preset: _p, ...rest } = update;\n if (rest.color !== undefined) parseColor(rest.color, 'paper color');\n const paper = normalizePaper({ ...rest, preset: base.preset }, base);\n const changedKeys = Object.keys(rest).filter((k) => k !== 'name');\n if (changedKeys.length && update.preset === undefined) paper.preset = base.preset;\n if (this.wet.world) this.beginWetOp();\n this.commit('Paper', { ...this.doc, paper });\n this.wet.paperChanged();\n return clonePaper(paper);\n }\n\n // --- Layers --------------------------------------------------------------------\n\n getLayer(id: string): PaintLayer | undefined {\n return findLayer(this.doc.layers, id)?.layer;\n }\n\n /** The active pixel layer (or `undefined`). */\n getActiveLayer(): PaintLayer | undefined {\n return this.doc.activeLayerId ? this.getLayer(this.doc.activeLayerId) : undefined;\n }\n\n /** Every layer, top first, as panels and tools describe them. */\n listLayers(): PaintLayerInfo[] {\n const out: PaintLayerInfo[] = [];\n const walk = (list: readonly PaintLayer[], parentId: string | null, depth: number) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const l = list[i];\n out.push(this.describeLayer(l, parentId, depth));\n if (l.type === 'group') walk(l.children, l.id, depth + 1);\n }\n };\n walk(this.doc.layers, null, 0);\n return out;\n }\n\n describeLayer(l: PaintLayer, parentId: string | null = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0): PaintLayerInfo {\n let bounds: RasterRect | null = null;\n if (l.type === 'pixel') {\n const b = contentBounds(l.surface);\n bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;\n } else for (const c of flattenLayers(l.children)) if (c.type === 'pixel') bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);\n return {\n id: l.id,\n name: l.name,\n type: l.type,\n visible: l.visible,\n opacity: l.opacity,\n blend: l.blend,\n clipped: Boolean(l.clipped),\n locks: { ...(l.locks ?? {}) },\n active: l.id === this.doc.activeLayerId,\n bounds,\n parentId,\n depth,\n };\n }\n\n private requireLayer(id: string): PaintLayer {\n const l = this.getLayer(id);\n if (!l) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n return l;\n }\n\n /** The pixel layer a paint operation targets, checked for locks and visibility. */\n private paintTarget(layerId: string | undefined, what: string): PaintPixelLayer {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) throw new PaintEditorError(`There is no active layer to ${what}; add one with add_layer.`);\n const l = this.requireLayer(id);\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; ${what} a pixel layer inside it.`);\n if (isLayerLocked(l, 'pixels')) throw new PaintEditorError(`Layer \"${l.name}\" has its pixels locked; unlock it (update_layer locks) before you ${what} it.`);\n if (!l.visible) throw new PaintEditorError(`Layer \"${l.name}\" is hidden; show it (update_layer visible true) before you ${what} it.`);\n return l;\n }\n\n setActiveLayer(id: string): void {\n this.requireLayer(id);\n if (this.doc.activeLayerId === id) return;\n // Selection of the active layer is editor state, not an undo step: keep the history's documents.\n this.doc = { ...this.doc, activeLayerId: id };\n this.emit();\n }\n\n /** Adds an empty pixel layer or group above `aboveId` (default: the active layer) and makes it active. */\n addLayer(options: { name?: string; type?: 'pixel' | 'group'; aboveId?: string; parentId?: string | null } = {}): PaintLayerInfo {\n this.requireIdle('adding a layer');\n const count = flattenLayers(this.doc.layers).length;\n const type = options.type ?? 'pixel';\n const name = options.name?.trim() || (type === 'group' ? `Group ${count + 1}` : `Layer ${count + 1}`);\n const layer: PaintLayer =\n type === 'group' ? { type: 'group', id: newPaintLayerId(), name, visible: true, opacity: 1, blend: 'pass-through', children: [] } : createPaintLayer(name);\n let parentId: string | null = null;\n let index: number | undefined;\n const above = options.aboveId ?? (options.parentId === undefined ? this.doc.activeLayerId : null);\n if (options.parentId !== undefined && options.parentId !== null) {\n const g = this.requireLayer(options.parentId);\n if (g.type !== 'group') throw new PaintEditorError(`Layer \"${g.name}\" is not a group; pass a group id as parentId.`);\n parentId = g.id;\n } else if (above) {\n const loc = findLayer(this.doc.layers, above);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${above}\". Call list_layers for the ids.`);\n parentId = loc.parent?.id ?? null;\n index = loc.index + 1;\n }\n const layers = insertLayer(this.doc.layers, layer, parentId, index);\n this.commit(type === 'group' ? 'Add group' : 'Add layer', { ...this.doc, layers, activeLayerId: layer.id });\n return this.describeLayer(layer);\n }\n\n updateLayer(id: string, update: LayerUpdate): PaintLayerInfo {\n this.requireIdle('changing a layer');\n const l = this.requireLayer(id);\n if (isLayerLocked(l, 'all') && Object.keys(update).some((k) => k !== 'locks')) throw new PaintEditorError(`Layer \"${l.name}\" is fully locked; unlock it first (locks {all: false}).`);\n if (update.opacity !== undefined && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);\n if (update.blend !== undefined && !isBlendMode(update.blend) && !(l.type === 'group' && update.blend === 'pass-through')) {\n throw new PaintEditorError(`Unknown blend mode \"${update.blend}\". Call get_canvas_info for the blend modes.`);\n }\n if (update.name !== undefined && !update.name.trim()) throw new PaintEditorError('name is empty; give the layer a name.');\n const next = { ...l } as PaintLayer;\n if (update.name !== undefined) next.name = update.name.trim();\n if (update.visible !== undefined) next.visible = update.visible;\n if (update.opacity !== undefined) next.opacity = update.opacity;\n if (update.blend !== undefined) (next as { blend: string }).blend = update.blend;\n if (update.clipped !== undefined) next.clipped = update.clipped || undefined;\n if (update.locks !== undefined) {\n const locks = { ...(l.locks ?? {}), ...update.locks };\n next.locks = Object.values(locks).some(Boolean) ? locks : undefined;\n }\n this.commit('Layer properties', { ...this.doc, layers: mapLayer(this.doc.layers, id, () => next) });\n return this.describeLayer(next);\n }\n\n removeLayer(id: string): { removed: string; activeLayerId: string | null } {\n this.requireIdle('removing a layer');\n const l = this.requireLayer(id);\n if (isLayerLocked(l, 'all')) throw new PaintEditorError(`Layer \"${l.name}\" is fully locked; unlock it before removing it.`);\n const wetIds = this.wet.wetLayerIds().filter((x) => x === id || (l.type === 'group' && findLayer(l.children, x)));\n if (wetIds.length) {\n this.beginWetOp();\n for (const x of wetIds) this.wet.world!.removeLane(x);\n }\n const layers = mapLayer(this.doc.layers, id, () => null);\n let active = this.doc.activeLayerId;\n if (active && !findLayer(layers, active)) active = flattenLayers(layers).filter((x) => x.type === 'pixel').pop()?.id ?? null;\n this.commit('Remove layer', { ...this.doc, layers, activeLayerId: active, ...this.thickWithout(flattenLayers([l]).map((x) => x.id)), ...this.stickersWithout(flattenLayers([l]).map((x) => x.id)) });\n return { removed: id, activeLayerId: active };\n }\n\n duplicateLayer(id: string): PaintLayerInfo {\n this.requireIdle('duplicating a layer');\n if (findLayer(this.doc.layers, id)) {\n const inside = (l: PaintLayer): string[] => (l.type === 'group' ? l.children.flatMap(inside) : [l.id]);\n this.dryLayersFirst(inside(findLayer(this.doc.layers, id)!.layer));\n }\n const loc = findLayer(this.doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const thick: Record<string, ThickChannels> = { ...(this.doc.thick ?? {}) };\n const stickers: Record<string, Sticker[]> = { ...(this.doc.stickers ?? {}) };\n const copy = (l: PaintLayer): PaintLayer => {\n const id = newPaintLayerId();\n if (thick[l.id]) thick[id] = thick[l.id];\n if (stickers[l.id]) stickers[id] = stickers[l.id].map((x) => ({ ...x }));\n return l.type === 'group' ? { ...l, id, children: l.children.map(copy) } : { ...l, id };\n };\n const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };\n const layers = insertLayer(this.doc.layers, dup, loc.parent?.id ?? null, loc.index + 1);\n this.commit('Duplicate layer', this.withWetEvents({ ...this.doc, layers, activeLayerId: dup.id, ...(this.doc.thick ? { thick } : {}), ...(this.doc.stickers ? { stickers } : {}) }));\n return this.describeLayer(dup);\n }\n\n /** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */\n moveLayer(id: string, index: number, parentId?: string | null): PaintLayerInfo {\n this.requireIdle('moving a layer');\n const loc = findLayer(this.doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const target = parentId === undefined ? (loc.parent?.id ?? null) : parentId;\n if (target) {\n const g = this.requireLayer(target);\n if (g.type !== 'group') throw new PaintEditorError(`Layer \"${g.name}\" is not a group; pass a group id as parentId.`);\n if (target === id || (loc.layer.type === 'group' && findLayer(loc.layer.children, target))) throw new PaintEditorError('A group cannot move into itself.');\n }\n const without = mapLayer(this.doc.layers, id, () => null);\n const siblings = target ? (findLayer(without, target)!.layer as { children: PaintLayer[] }).children : without;\n if (!Number.isInteger(index) || index < 0 || index > siblings.length) throw new PaintEditorError(`index ${index} is out of range; use 0..${siblings.length} (0 is the bottom).`);\n const layers = insertLayer(without, loc.layer, target, index);\n this.commit('Move layer', { ...this.doc, layers });\n return this.describeLayer(loc.layer);\n }\n\n /** Merges a pixel layer into the pixel layer below it (keeping the lower one's settings). */\n mergeDown(id: string): PaintLayerInfo {\n this.requireIdle('merging');\n const pre = findLayer(this.doc.layers, id);\n if (pre) {\n const sib = pre.parent ? pre.parent.children : this.doc.layers;\n this.dryLayersFirst([id, sib[pre.index - 1]?.id].filter((x): x is string => Boolean(x)));\n }\n const loc = findLayer(this.doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const siblings = loc.parent ? loc.parent.children : this.doc.layers;\n const below = siblings[loc.index - 1];\n const upper = loc.layer;\n if (!isPixelLayer(upper) || !isPixelLayer(below)) throw new PaintEditorError('Merge down needs a pixel layer directly above another pixel layer in the same group.');\n if (isLayerLocked(below, 'pixels')) throw new PaintEditorError(`Layer \"${below.name}\" has its pixels locked; unlock it to merge into it.`);\n const rect = unionRect(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);\n let surface = emptySurface(4);\n if (rect) {\n const base: PaintPixelLayer = { ...below, opacity: 1, blend: 'normal', visible: true, clipped: undefined, mask: undefined };\n // Thick paint on the upper layer merges as it is lit (its relief does not carry over).\n const upperLit = this.litLayers([upper])[0] as PaintPixelLayer;\n const px = createPaintCompositor().composite([base, upper.visible ? upperLit : { ...upperLit, opacity: 0 }] as ViewLayer[], rect, this.sources());\n surface = surfaceFromPixels({ width: rect.width, height: rect.height, data: px }, 4, rect.x, rect.y);\n }\n const merged: PaintPixelLayer = { ...below, surface, offset: { x: 0, y: 0 } };\n const layers = mapLayer(mapLayer(this.doc.layers, upper.id, () => null), below.id, () => merged);\n this.commit('Merge down', this.withWetEvents({ ...this.doc, layers, activeLayerId: below.id, ...this.thickWithout([upper.id]), ...this.stickersWithout([upper.id, below.id]) }));\n return this.describeLayer(merged);\n }\n\n /** Removes every pixel of a layer. */\n clearLayer(layerId?: string): { layerId: string; bounds: RasterRect | null } {\n this.requireIdle('clearing');\n const l = this.paintTarget(layerId, 'clear');\n if (this.wet.wetLayerIds().includes(l.id)) {\n this.beginWetOp();\n this.wet.world!.removeLane(l.id);\n }\n const bounds = this.describeLayer(l).bounds;\n this.commit('Clear layer', this.withPaintEvent({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })), ...this.thickWithout([l.id]), ...this.stickersWithout([l.id]) }, { kind: 'clear', layerId: l.id }));\n return { layerId: l.id, bounds };\n }\n\n /**\n * Fills a layer across the canvas with a colour. With `grain` above 0 the fill follows the paper\n * grain (it catches the peaks, as pastel or charcoal rubbed across the sheet).\n */\n fillLayer(options: FillLayerOptions = {}): { layerId: string; bounds: RasterRect } {\n this.requireIdle('filling');\n const fillTarget = this.paintTarget(options.layerId, 'fill');\n if (this.doc.stickers?.[fillTarget.id]?.length) throw new PaintEditorError(`Layer \"${fillTarget.name}\" is a sticker layer; fill another layer (add_layer).`);\n this.dryLayersFirst([options.layerId ?? this.doc.activeLayerId ?? '']);\n const l = this.paintTarget(options.layerId, 'fill');\n const color = options.color === undefined ? this.color : parseColor(options.color);\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new PaintEditorError(`opacity ${opacity} is out of range; use 0..1.`);\n const grainAmount = options.grain ?? 0;\n if (!(grainAmount >= 0 && grainAmount <= 1)) throw new PaintEditorError(`grain ${grainAmount} is out of range; use 0..1.`);\n const pressure = options.pressure ?? 0.5;\n const blend = options.blend ?? 'normal';\n if (!isBlendMode(blend)) throw new PaintEditorError(`Unknown blend mode \"${blend}\".`);\n const { width, height } = this.doc.canvas;\n const rect = { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const depth = grainAmount * this.doc.paper.depth;\n const grain = depth > 0 ? paperGrain(this.doc.paper) : null;\n for (const { tx, ty } of tilesInRect(rect)) {\n const r = { x: tx * TILE, y: ty * TILE, width: Math.min(TILE, width - tx * TILE), height: Math.min(TILE, height - ty * TILE) };\n const vals = new Float32Array(r.width * r.height);\n for (let y = 0, k = 0; y < r.height; y++) for (let x = 0; x < r.width; x++, k++) vals[k] = grain ? grainCatch(grain.at(r.x + x + 0.5, r.y + y + 0.5), depth, pressure) : 1;\n coverage.accumulate(r, vals, 1);\n }\n const apply: StrokeApply = {\n mode: 'paint',\n color: rgbaOf(color),\n background: rgbaOf(this.doc.paper.color),\n opacity,\n blend,\n lockTransparency: isLayerLocked(l, 'transparency'),\n selection: null,\n offset: l.offset,\n };\n const surface = this.applyCoverage(l, coverage, rect, apply);\n this.commit('Fill layer', this.withPaintEvent(this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })) }), { kind: 'fill', layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }));\n return { layerId: l.id, bounds: rect };\n }\n\n private applyCoverage(l: PaintPixelLayer, coverage: CoverageMap, rect: RasterRect, apply: StrokeApply): TiledSurface {\n const base = l.surface;\n const tiles = new Map(base.tiles);\n for (const { tx, ty } of tilesInRect({ x: rect.x - l.offset.x, y: rect.y - l.offset.y, width: rect.width, height: rect.height })) {\n const key = tileKey(tx, ty);\n const src = base.tiles.get(key)?.data ?? new Uint8ClampedArray(TILE * TILE * 4);\n const tile = finishTile(applyStrokeToTile(coverage, src, 4, tx, ty, apply), 4, 0);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n return new TiledSurface(4, tiles, 0);\n }\n\n // --- Brushes -------------------------------------------------------------------\n\n private findBrush(id: string): PaintBrush | undefined {\n return BUILTIN_BRUSHES.find((b) => b.id === id) ?? this.userBrushes.find((b) => b.id === id);\n }\n\n /** A library brush by id (`undefined` when unknown). */\n getBrush(id: string): PaintBrush | undefined {\n const b = this.findBrush(id);\n return b ? normalizePaintBrush(b, b) : undefined;\n }\n\n private requireBrush(id: string): PaintBrush {\n const b = this.findBrush(id);\n if (!b) throw new PaintEditorError(`Unknown brush \"${id}\". Call list_brushes for the ids.`);\n return b;\n }\n\n /**\n * Chooses the active brush (`id`: a library brush) and / or changes its settings. Editor state,\n * not an undo step. Returns the active brush.\n */\n setBrush(update: { id?: string } & Omit<PaintBrushInput, 'id'> = {}): PaintBrush {\n const { id, ...changes } = update;\n const base = id !== undefined ? this.requireBrush(id) : this.brush;\n // Gradient paint is checked strictly here (documents and presets are read leniently).\n if (changes.paint !== undefined && changes.paint !== null) this.readStrokePaint(changes.paint);\n const next = normalizePaintBrush({ ...changes, id: base.id }, base);\n // The active brush is `builtin` only while it is an unchanged built-in preset.\n if (base.builtin && !Object.keys(changes).length) next.builtin = true;\n else delete next.builtin;\n this.brush = next;\n this.emit([]);\n return this.brush;\n }\n\n /** Saves the active brush (or `brush`) into the user library under `id` / `name`. */\n saveBrush(options: { id: string; name?: string; brush?: PaintBrushInput }): PaintBrush {\n if (!BRUSH_ID.test(options.id)) throw new PaintEditorError(`Brush id \"${options.id}\" is invalid; use lowercase letters, digits and dashes (e.g. \"my-pencil\").`);\n if (BUILTIN_BRUSHES.some((b) => b.id === options.id)) throw new PaintEditorError(`\"${options.id}\" is a built-in brush; save under another id.`);\n const source = options.brush ? normalizePaintBrush(options.brush, this.brush) : this.brush;\n const saved = normalizePaintBrush({ ...source, id: options.id, name: options.name ?? source.name });\n delete saved.builtin;\n const i = this.userBrushes.findIndex((b) => b.id === options.id);\n if (i >= 0) this.userBrushes[i] = saved;\n else this.userBrushes.push(saved);\n this.library = null;\n this.emit([]);\n return saved;\n }\n\n removeBrush(id: string): void {\n if (BUILTIN_BRUSHES.some((b) => b.id === id)) throw new PaintEditorError(`\"${id}\" is a built-in brush and cannot be removed.`);\n const i = this.userBrushes.findIndex((b) => b.id === id);\n if (i < 0) throw new PaintEditorError(`Unknown brush \"${id}\". Call list_brushes for the ids.`);\n this.userBrushes.splice(i, 1);\n this.library = null;\n this.emit([]);\n }\n\n /** The user's saved brushes. */\n getUserBrushes(): PaintBrush[] {\n return this.userBrushes.map((b) => normalizePaintBrush(b, b));\n }\n\n /** A brush library file of the given brushes (default: the user's brushes). */\n exportBrushes(ids?: readonly string[]): BrushLibraryFile {\n const list = ids ? ids.map((id) => this.requireBrush(id)) : this.userBrushes;\n return { format: 'pixerate-brushes', version: 1, brushes: list.map(brushToJSON) };\n }\n\n /**\n * Adds the brushes of a library file to the user library. A brush whose id is taken is skipped\n * unless `replace` (built-in ids are always skipped). Returns the ids added and the problems.\n */\n importBrushes(file: unknown, options: { replace?: boolean } = {}): { added: string[]; skipped: string[]; problems: string[] } {\n const problems: string[] = [];\n let list: PaintBrush[];\n try {\n list = parseBrushLibrary(file, problems);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n const added: string[] = [];\n const skipped: string[] = [];\n for (const b of list) {\n if (BUILTIN_BRUSHES.some((x) => x.id === b.id)) {\n skipped.push(b.id);\n problems.push(`\"${b.id}\" is a built-in brush id; rename it to import it.`);\n continue;\n }\n const i = this.userBrushes.findIndex((x) => x.id === b.id);\n if (i >= 0 && !options.replace) {\n skipped.push(b.id);\n continue;\n }\n if (i >= 0) this.userBrushes[i] = b;\n else this.userBrushes.push(b);\n added.push(b.id);\n }\n if (added.length) {\n this.library = null;\n this.emit([]);\n }\n return { added, skipped, problems };\n }\n\n /** A thumbnail of a brush: a test stroke on the current paper. */\n brushThumbnail(id: string, options: { width?: number; height?: number; color?: string } = {}): BrushThumbnail {\n const b = id === this.brush.id ? this.brush : this.requireBrush(id);\n return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });\n }\n\n // --- Colour --------------------------------------------------------------------\n\n /** Sets the paint colour (and goes back to one colour from gradient paint). */\n setColor(color: string): string {\n this.color = parseColor(color);\n this.gradient = null;\n this.emit([]);\n return this.color;\n }\n\n /**\n * Sets what strokes paint with: one colour (`{ type: 'color', color }` or `#rrggbb`), or gradient\n * paint (`{ type: 'gradient', stops, space?, map?, softness? }`, see `@pixerate/editor/paint`),\n * which brushes sample dab by dab as the stroke goes. Not an undo step. Throws with what is wrong\n * with an invalid source. Returns the paint.\n */\n setPaint(paint: PaintSource | string): PaintSource {\n let p: PaintSource;\n try {\n p = parsePaintSource(paint);\n } catch (e) {\n if (e instanceof PaintSourceError) throw new PaintEditorError(e.message);\n throw e;\n }\n if (p.type === 'color') {\n this.setColor(p.color);\n return p;\n }\n this.gradient = p;\n this.emit([]);\n return p;\n }\n\n /**\n * The gradient a stroke paints (resolved against its colours: no `paint` / `secondary` stops),\n * or `null` for one colour: the stroke's own `paint`, else (without an explicit `color`) the\n * brush's gradient, else the editor's gradient paint. Erasers, blenders and smudges take none.\n */\n private strokeGradient(init: PaintStrokeInit, brush: PaintBrush, color: string): PaintGradientSource | null {\n if (!brushTakesPaint(brush)) return null;\n let g: PaintGradientSource | null = null;\n if (init.paint !== undefined) {\n const p = this.readStrokePaint(init.paint);\n g = p.type === 'gradient' ? p : null;\n } else g = brush.paint ?? (init.color === undefined ? this.gradient : null);\n return g ? resolvePaintGradient(g, { paint: color, secondary: this.secondaryColor }) : null;\n }\n\n private readStrokePaint(raw: unknown): PaintSource {\n try {\n return parsePaintSource(raw);\n } catch (e) {\n if (e instanceof PaintSourceError) throw new PaintEditorError(e.message);\n throw e;\n }\n }\n\n setSecondaryColor(color: string): string {\n this.secondaryColor = parseColor(color);\n this.emit([]);\n return this.secondaryColor;\n }\n\n swapColors(): void {\n [this.color, this.secondaryColor] = [this.secondaryColor, this.color];\n this.emit([]);\n }\n\n /** The colour at a document pixel: of the composite (paper included) or of one layer. */\n pickColor(x: number, y: number, options: { layerId?: string } = {}): { color: string; alpha: number } {\n const { width, height } = this.doc.canvas;\n const px = Math.floor(x);\n const py = Math.floor(y);\n if (!(px >= 0 && py >= 0 && px < width && py < height)) throw new PaintEditorError(`(${x}, ${y}) is outside the ${width}×${height} canvas.`);\n if (options.layerId !== undefined) {\n const l = this.requireLayer(options.layerId);\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; pick from a pixel layer or the composite.`);\n const [r, g, b, a] = readPixel(l.surface, px - l.offset.x, py - l.offset.y);\n return { color: rgbToHex(r, g, b), alpha: Math.round((a / 255) * 1000) / 1000 };\n }\n const d = this.exportCompositor.composite(this.renderLayers({}), { x: px, y: py, width: 1, height: 1 }, this.sources());\n return { color: rgbToHex(d[0], d[1], d[2]), alpha: Math.round((d[3] / 255) * 1000) / 1000 };\n }\n\n // --- Mixer Brush ---------------------------------------------------------------\n\n /** Fills the Mixer Brush reservoir with a colour (default the editor colour). */\n loadMixerBrush(color?: string): MixerLoad {\n const [r, g, b] = hexToRgb8(color === undefined ? this.color : parseColor(color));\n this.mixerLoad = loadMixer(this.mixerLoad, [r / 255, g / 255, b / 255]);\n this.emit([]);\n return this.mixerLoad;\n }\n\n /** Empties the pickup well (and with `emptyReservoir` the loaded paint too). */\n cleanMixerBrush(emptyReservoir = false): MixerLoad {\n this.mixerLoad = emptyReservoir ? EMPTY_MIXER_LOAD : cleanMixer(this.mixerLoad);\n this.emit([]);\n return this.mixerLoad;\n }\n\n // --- View and hand -------------------------------------------------------------\n\n /** Zoom, pan, rotation and mirroring: view state only, the pixels never resample. */\n setView(update: Partial<PaintView>): PaintView {\n this.view = normalizeView(update, this.view);\n this.emit([]);\n return this.view;\n }\n\n resetView(): PaintView {\n return this.setView({ ...DEFAULT_VIEW, pan: { x: 0, y: 0 } });\n }\n\n setHand(hand: Hand): Hand {\n if (hand !== 'left' && hand !== 'right') throw new PaintEditorError(`hand is ${JSON.stringify(hand)}; use \"left\" or \"right\".`);\n this.hand = hand;\n this.emit([]);\n return hand;\n }\n\n // --- Strokes -------------------------------------------------------------------\n\n private strokeBrush(init: PaintStrokeInit): PaintBrush {\n let brush = init.brushId !== undefined ? this.requireBrush(init.brushId) : this.brush;\n if (init.brush) brush = normalizePaintBrush({ ...init.brush, id: brush.id }, brush);\n return brush;\n }\n\n /**\n * After a stroke: a Mixer Brush that loads colour keeps its whole load; any other dirty brush\n * keeps only the paint it carries (the reservoir stays the Mixer Brush's).\n */\n private keepLoad(brush: PaintBrush, load: MixerLoad): void {\n if (brush.engine === 'mixer' && brush.loadsColor) this.mixerLoad = load;\n else if (brush.behavior.dirty) this.mixerLoad = { ...this.mixerLoad, pickup: load.pickup };\n }\n\n /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */\n protected strokeCopies(): Affine[] {\n return symmetryCopies(this.guides.symmetry);\n }\n\n private sessionFor(doc: PaintDocumentState, init: PaintStrokeInit, host: StrokeSessionHost): PaintStrokeSession {\n if (init.paint !== undefined) {\n const p = this.readStrokePaint(init.paint);\n if (p.type === 'color') init = { ...init, color: p.color, paint: undefined };\n else init = { ...init, paint: p };\n }\n let layer = (() => {\n const id = init.layerId ?? doc.activeLayerId;\n if (!id) throw new PaintEditorError('There is no active layer to paint on; add one with add_layer.');\n const loc = findLayer(doc.layers, id);\n if (!loc) throw new PaintEditorError(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const l = loc.layer;\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; paint on a pixel layer inside it.`);\n if (isLayerLocked(l, 'pixels')) throw new PaintEditorError(`Layer \"${l.name}\" has its pixels locked; unlock it (update_layer locks) to paint on it.`);\n if (!l.visible) throw new PaintEditorError(`Layer \"${l.name}\" is hidden; show it (update_layer visible true) to paint on it.`);\n return l;\n })();\n const brush = this.strokeBrush(init);\n const stickerLayer = Boolean(doc.stickers?.[layer.id]?.length);\n if (brush.engine === 'sticker') {\n if (!stickerLayer && contentBounds(layer.surface)) throw new PaintEditorError(`Layer \"${layer.name}\" already has paint; spray stickers onto an empty layer (add_layer) so they stay editable.`);\n } else if (stickerLayer) {\n throw new PaintEditorError(`Layer \"${layer.name}\" is a sticker layer (its pixels are drawn from its stickers); paint on another layer (add_layer).`);\n }\n let wet: { driver: WetStrokeDriver; surface: () => TiledSurface } | undefined;\n if (isWetBrush(brush)) {\n this.requireWetIdle('painting');\n if (layer.offset.x || layer.offset.y) {\n // The wet simulation works in canvas space: move the pixels into it (same picture, not an undo step).\n const moved: PaintPixelLayer = { ...layer, surface: translateSurface(layer.surface, layer.offset.x, layer.offset.y), offset: { x: 0, y: 0 } };\n this.doc = { ...this.doc, layers: mapLayer(this.doc.layers, layer.id, () => moved) };\n if (doc !== this.doc) doc = { ...doc, layers: mapLayer(doc.layers, layer.id, () => moved) };\n layer = moved;\n }\n if (!this.wet.wetLayerIds().includes(layer.id) && this.wet.wetLayerIds().length >= MAX_WET_LANES) {\n throw new PaintEditorError(`At most ${MAX_WET_LANES} layers can hold wet paint at once; dry one first (dry_paint).`);\n }\n this.beginWetOp();\n const world = this.wet.ensureLane(layer.id, brush.wet.medium);\n const id = layer.id;\n const wetColor = init.color === undefined ? this.color : parseColor(init.color);\n const [r, g, b] = hexToRgb8(wetColor);\n const wetGradient = this.strokeGradient(init, brush, wetColor);\n const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);\n const seedW = init.seed ?? mix32((this.seedCounter * 0x9e3779b1) >>> 0) & 0x7fffffff;\n const copiesW = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));\n const driver = new WetStrokeDriver({\n world,\n laneId: id,\n brush,\n color: [r / 255, g / 255, b / 255],\n load,\n grain: paperGrain(doc.paper),\n paperDepth: doc.paper.depth,\n paperColor: hexToRgb8(doc.paper.color),\n seed: seedW,\n copies: [IDENTITY_AFFINE, ...copiesW].map(affineCopy),\n mask: init.maskStencils ? this.stencilSelection(init.maskStencils) : init.stencils !== false ? this.stencilSelection() : null,\n paint: wetGradient,\n });\n init = { ...init, seed: seedW, load };\n wet = { driver, surface: () => world.display(id).surface };\n } else if (this.wet.wetLayerIds().includes(layer.id)) {\n // A dry brush on wet paint: the paint dries first (in the same undo step).\n this.dryLayersFirst([layer.id]);\n const dried = findLayer(this.doc.layers, layer.id)!.layer as PaintPixelLayer;\n layer = dried;\n }\n let color = init.color === undefined ? this.color : parseColor(init.color);\n const tracing = this.guides.tracing;\n if (init.color === undefined && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;\n const perspective = this.guides.perspective;\n const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;\n const useStencils = init.stencils !== false;\n const stencilSnapper = useStencils && this.guides.stencils.some((x) => x.enabled && x.mode === 'guide') ? new StencilSnapper(this.guides.stencils) : null;\n const selection = init.maskStencils ? this.stencilSelection(init.maskStencils) : useStencils ? this.stencilSelection() : null;\n const inputFilter = snapper || stencilSnapper ? (p: PenPoint) => (stencilSnapper ? stencilSnapper.snap(snapper ? snapper.snap(p) : p) : snapper!.snap(p)) : undefined;\n const rgba = rgbaOf(color);\n const seed = init.seed ?? mix32((this.seedCounter++ * 0x9e3779b1) >>> 0) & 0x7fffffff;\n const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));\n const session = new PaintStrokeSession(host, {\n layerId: layer.id,\n surface: layer.surface,\n offset: layer.offset,\n lockTransparency: isLayerLocked(layer, 'transparency'),\n brush,\n color: rgba,\n background: rgbaOf(doc.paper.color),\n grain: paperGrain(doc.paper),\n paperDepth: doc.paper.depth,\n seed,\n colorHex: color,\n affineCopies: copies,\n copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),\n inputFilter,\n selection,\n mixerLoad: init.load ?? brushLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]),\n secondary: rgbaOf(this.secondaryColor),\n wet,\n thick: brush.engine === 'impasto' ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : undefined,\n sticker: brush.engine === 'sticker' ? { nextId: this.nextStickerNumber(doc) } : undefined,\n paint: this.strokeGradient(init, brush, color),\n });\n if (wet) this.wet.driver = wet.driver;\n if (init.maskStencils) session.maskStencils = init.maskStencils;\n return session;\n }\n\n private record(session: PaintStrokeSession): RecordedStroke {\n const init = session.init;\n const stroke: RecordedStroke = {\n id: `s${this.strokeCounter++}`,\n layerId: session.layerId,\n brush: brushToJSON(init.brush) as PaintBrush,\n color: init.colorHex,\n points: session.points.map((p) => ({ ...p })),\n seed: init.seed,\n };\n if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m] as unknown as Affine);\n if (init.paint) stroke.paint = JSON.parse(JSON.stringify(init.paint));\n if (init.mixerLoad.reservoir || init.mixerLoad.pickup) stroke.load = JSON.parse(JSON.stringify(init.mixerLoad));\n if (init.wet) stroke.steps = [...init.wet.driver.steps];\n if (init.selection) {\n const masks = (session.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === 'mask');\n if (masks.length) stroke.stencils = masks.map((x) => ({ ...x }));\n }\n return stroke;\n }\n\n /**\n * Starts a live stroke for pointer input: `push` raw pen samples (document pixels), then `end()`\n * (one undo step) or `cancel()`.\n */\n beginStroke(init: PaintStrokeInit = {}): PaintStrokeSession {\n this.requireIdle('starting another');\n const before = this.doc;\n const host: StrokeSessionHost = {\n update: (layerId, surface, rects) => {\n if (this.session?.init.wet) {\n // The wet layer's display is the layer's pixels: show it (not an undo step).\n this.wet.syncDisplays();\n return;\n }\n this.live = { layerId, surface };\n const ch = this.session?.thickChannels();\n this.liveThick = ch ? { layerId, channels: ch } : null;\n this.emit(rects);\n },\n commit: (s, surface, cancelled) => {\n this.session = null;\n this.live = null;\n this.liveLayers = null;\n this.liveThick = null;\n const wetStroke = Boolean(s.init.wet);\n if (wetStroke) this.wet.driver = null;\n const brush = s.init.brush;\n if (!cancelled && s.result?.mixerLoad) this.keepLoad(brush, s.result.mixerLoad);\n if (cancelled || !surface) {\n if (this.wetOp) {\n // Put the wet paint back as it was before the stroke.\n const snap = this.wetOp.before;\n this.wetOp = null;\n this.pendingWetEvents = [];\n if (wetStroke) this.wet.restore(snap);\n this.doc = { ...this.doc, layers: before.layers };\n this.wet.syncDisplays();\n }\n this.emit();\n return;\n }\n const stroke = this.record(s);\n s.result!.strokeId = stroke.id;\n if (wetStroke && this.wet.instantDry) {\n this.wet.dry(s.layerId);\n this.logWet({ kind: 'dry', layerId: s.layerId, at: this.doc.strokes.length + 1 });\n surface = (this.getLayer(s.layerId) as PaintPixelLayer).surface;\n s.result!.wet = this.wet.isWet(s.layerId);\n }\n const l = this.getLayer(s.layerId) as PaintPixelLayer;\n const ch = s.thickChannels();\n const thick = ch ? { ...(this.doc.thick ?? {}), [l.id]: ch } : this.doc.thick;\n const sprayed = s.stickers();\n const stickers = sprayed ? { ...(this.doc.stickers ?? {}), [l.id]: [...(this.doc.stickers?.[l.id] ?? []), ...sprayed] } : this.doc.stickers;\n this.commit(`${brush.name} stroke`, this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...(thick ? { thick } : {}), ...(stickers ? { stickers } : {}) }), wetStroke ? before : this.doc);\n },\n };\n let session: PaintStrokeSession;\n try {\n session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);\n } catch (e) {\n this.wetOp = null;\n throw e;\n }\n this.session = session;\n this.emit([]);\n return session;\n }\n\n /** The current live stroke, if any. */\n get activeStroke(): PaintStrokeSession | null {\n return this.session;\n }\n\n /** Runs a whole stroke against `doc` without touching the editor; returns the new document. */\n private runStroke(doc: PaintDocumentState, init: PaintStrokeInit, points: readonly PenPoint[]): { doc: PaintDocumentState; result: PaintStrokeResult; brush: PaintBrush } {\n let out = doc;\n const host: StrokeSessionHost = {\n update: () => {},\n commit: (s, surface, cancelled) => {\n if (cancelled || !surface) return;\n const stroke = this.record(s);\n s.result!.strokeId = stroke.id;\n const l = findLayer(out.layers, s.layerId)!.layer as PaintPixelLayer;\n const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;\n const ch = s.thickChannels();\n const sprayed = s.stickers();\n out = {\n ...out,\n layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),\n strokes: [...out.strokes, stroke],\n ...(ch ? { thick: { ...(out.thick ?? {}), [l.id]: ch } } : {}),\n ...(sprayed ? { stickers: { ...(out.stickers ?? {}), [l.id]: [...(out.stickers?.[l.id] ?? []), ...sprayed] } } : {}),\n };\n },\n };\n const session = this.sessionFor(doc, init, host);\n session.push(points);\n const result = session.end();\n if (session.init.wet) {\n this.wet.driver = null;\n if (this.wet.instantDry && result.strokeId) {\n this.wet.dry(session.layerId);\n this.logWet({ kind: 'dry', layerId: session.layerId, at: out.strokes.length });\n const dried = (findLayer(this.doc.layers, session.layerId)!.layer as PaintPixelLayer).surface;\n out = { ...out, layers: mapLayer(out.layers, session.layerId, (l) => ({ ...(l as PaintPixelLayer), surface: dried })) };\n }\n result.wet = this.wet.isWet(session.layerId);\n }\n return { doc: out, result, brush: session.init.brush };\n }\n\n /** Paints one stroke from data (one undo step). */\n paintStroke(init: PaintStrokeInit & { points: readonly PenPoint[] }): PaintStrokeResult {\n this.requireIdle('painting');\n const points = this.checkPoints(init.points);\n const before = this.doc;\n let run: ReturnType<PaintEditorController['runStroke']>;\n try {\n run = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);\n } catch (e) {\n this.wetOp = null;\n throw e;\n }\n const { doc, result, brush } = run;\n if (result.mixerLoad) this.keepLoad(brush, result.mixerLoad);\n if (doc !== before || this.wetOp) this.commit(`${brush.name} stroke`, this.withWetEvents(doc), before);\n else this.emit([]);\n return result;\n }\n\n /** Checks stroke samples (finite x and y, pressure 0..1) and returns them; throws a sentence naming the bad point. */\n checkPoints(points: readonly PenPoint[]): PenPoint[] {\n if (!Array.isArray(points) || !points.length) throw new PaintEditorError('points is empty; give at least one {x, y} in document pixels.');\n const out: PenPoint[] = [];\n points.forEach((p, i) => {\n if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) throw new PaintEditorError(`points[${i}] needs finite x and y in document pixels.`);\n if (p.pressure !== undefined && !(p.pressure >= 0 && p.pressure <= 1)) throw new PaintEditorError(`points[${i}].pressure ${p.pressure} is out of range; use 0..1.`);\n out.push(p);\n });\n return out;\n }\n\n /** Every recorded stroke, oldest first. */\n getStrokes(): readonly RecordedStroke[] {\n return this.doc.strokes;\n }\n\n /**\n * Replays recorded strokes, varied (moved, scaled, rotated, flipped, recoloured, with another\n * brush or layer), `repeat` times: all of it one undo step. Returns the new strokes.\n */\n replayStrokes(options: ReplayOptions): { strokeIds: string[]; bounds: RasterRect | null; layerIds: string[] } {\n this.requireIdle('replaying');\n if (!options.strokeIds?.length) throw new PaintEditorError('strokeIds is empty; pass ids from list_strokes.');\n const strokes = options.strokeIds.map((id) => {\n const s = this.doc.strokes.find((x) => x.id === id);\n if (!s) throw new PaintEditorError(`Unknown stroke \"${id}\". Call list_strokes for the ids.`);\n return s;\n });\n const repeat = options.repeat ?? 1;\n if (!Number.isInteger(repeat) || repeat < 1 || repeat > 100) throw new PaintEditorError(`repeat ${repeat} is out of range; use 1..100.`);\n if (options.scale !== undefined && !(options.scale > 0 && options.scale <= 20)) throw new PaintEditorError(`scale ${options.scale} is out of range; use above 0 up to 20.`);\n if (options.color !== undefined) parseColor(options.color);\n if (options.brushId !== undefined) this.requireBrush(options.brushId);\n if (options.layerId !== undefined) this.paintTarget(options.layerId, 'replay onto');\n let origin = options.origin;\n if (!origin) {\n const b = strokeBounds(strokes.flatMap((s) => s.points));\n origin = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n }\n const step = variationAffine(options, origin);\n let doc = this.doc;\n let bounds: RasterRect | null = null;\n const ids: string[] = [];\n const layerIds = new Set<string>();\n let m: Affine = IDENTITY_AFFINE;\n for (let k = 0; k < repeat; k++) {\n m = [step[0] * m[0] + step[1] * m[2], step[0] * m[1] + step[1] * m[3], step[2] * m[0] + step[3] * m[2], step[2] * m[1] + step[3] * m[3], step[0] * m[4] + step[1] * m[5] + step[4], step[2] * m[4] + step[3] * m[5] + step[5]];\n for (const s of strokes) {\n const v = varyStroke(s, m, options);\n let brush = v.brush;\n if (options.brushId !== undefined) {\n const b = this.requireBrush(options.brushId);\n brush = normalizePaintBrush({ tip: { ...b.tip, size: b.tip.size * (v.brush.tip.size / s.brush.tip.size) } }, b);\n }\n const layerId = options.layerId ?? s.layerId;\n if (!findLayer(doc.layers, layerId)) throw new PaintEditorError(`Stroke \"${s.id}\" was painted on layer \"${layerId}\", which no longer exists; pass layerId to replay it elsewhere.`);\n // The recorded load replays the stroke exactly; a recolour reloads the reservoir with the new colour.\n let load = s.load;\n if (load && options.color !== undefined && load.reservoir) {\n const [r, g, b] = hexToRgb8(options.color);\n load = { ...load, reservoir: [r / 255, g / 255, b / 255] };\n }\n // Gradient paint replays as recorded; a recolour paints the one new colour.\n const paint = v.paint && options.color === undefined ? { paint: v.paint } : {};\n const run = this.runStroke(doc, { layerId, color: options.color ?? s.color, brush, seed: s.seed, copies: v.copies ?? [], stencils: false, maskStencils: [], ...paint, ...(load && options.brushId === undefined ? { load } : {}) }, v.points);\n doc = run.doc;\n bounds = unionRect(bounds, run.result.bounds);\n if (run.result.strokeId) ids.push(run.result.strokeId);\n layerIds.add(layerId);\n }\n }\n if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? 'Replay stroke' : 'Replay strokes', doc);\n return { strokeIds: ids, bounds, layerIds: [...layerIds] };\n }\n\n // --- Thick paint and light -------------------------------------------------------\n\n /** The document's `thick` without these layers (as a spread: `{}` when unchanged). */\n private thickWithout(ids: readonly string[]): { thick?: Record<string, ThickChannels> } {\n const t = this.doc.thick;\n if (!t || !ids.some((id) => t[id])) return {};\n const out = { ...t };\n for (const id of ids) delete out[id];\n return { thick: out };\n }\n\n /** Where the light comes from for thick paint and metallic paper. */\n getLighting(): PaintLighting {\n return { ...this.lighting };\n }\n\n /** Moves the light (thick paint and metallic paper are relit; nothing is repainted). Saved with the document, not an undo step. */\n setLighting(update: Partial<PaintLighting>): PaintLighting {\n if (update.elevation !== undefined && !(update.elevation >= 5 && update.elevation <= 90)) throw new PaintEditorError(`elevation ${update.elevation} is out of range; use 5..90 degrees.`);\n if (update.relief !== undefined && !(update.relief >= 0 && update.relief <= 1)) throw new PaintEditorError(`relief ${update.relief} is out of range; use 0..1.`);\n this.lighting = normalizeLighting(update, this.lighting);\n this.emit('all');\n return { ...this.lighting };\n }\n\n /** A layer's relief: where it is, its highest point (0..255) and its tiles; `null` when it has none. */\n reliefOf(layerId?: string): { layerId: string; bounds: RasterRect | null; maxHeight: number } | null {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) return null;\n this.requireLayer(id);\n const ch = this.doc.thick?.[id];\n if (!ch) return null;\n const r = reliefInfo(ch);\n return { layerId: id, bounds: r.bounds, maxHeight: r.maxHeight };\n }\n\n /** Flattens a layer's thick paint (its colour stays; the relief and material go). One undo step. */\n flattenRelief(layerId?: string): { layerId: string; flattened: boolean } {\n this.requireIdle('flattening');\n const l = this.paintTarget(layerId, 'flatten');\n const t = this.thickWithout([l.id]);\n if (!t.thick) return { layerId: l.id, flattened: false };\n this.commit('Flatten relief', this.withPaintEvent({ ...this.doc, ...t }, { kind: 'flatten', layerId: l.id }));\n return { layerId: l.id, flattened: true };\n }\n\n // --- Replay: time-lapse and high-resolution export ------------------------------\n\n /**\n * A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain\n * scaled; every pixel layer empty; the same brushes, lighting and wet options). Dispose of it\n * when done.\n */\n createReplayTarget(scale = 1): PaintEditorController {\n if (!(scale >= 1 && scale <= 8)) throw new PaintEditorError(`scale ${scale} is out of range; use 1..8.`);\n const { width, height, dpi } = this.doc.canvas;\n const w = Math.round(width * scale);\n const h = Math.round(height * scale);\n if (w > 16384 || h > 16384) throw new PaintEditorError(`At scale ${scale} the canvas would be ${w}×${h}; the limit is 16384 px per side. Use a smaller scale.`);\n const target = new PaintEditorController({ width: w, height: h, dpi: dpi * scale, brushes: this.userBrushes, wet: this.wetOptions });\n const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };\n target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });\n target.lighting = { ...this.lighting };\n return target;\n }\n\n /**\n * Repaints the painting from its recorded strokes and events, as a time-lapse: PNG frames (after\n * strokes, and while wet paint flows), a `VideoProject` of the frames as stills and the ffmpeg\n * command that renders it (the video editor's export path). Changes nothing here.\n */\n async timeLapse(options: TimeLapseOptions = {}): Promise<TimeLapse> {\n const target = this.createReplayTarget(1);\n try {\n return await buildTimeLapse(this.doc, target, options);\n } finally {\n target.dispose();\n }\n }\n\n /**\n * The painting repainted from its recorded strokes at `scale` times the size (brush sizes,\n * paper grain, fills and wet events scaled), wet paint dried: more detail instead of upscaled\n * pixels. Layers painted without recorded strokes (imported pixels) come out empty. Changes nothing here.\n */\n /** `renderHighRes` as PNG bytes. */\n async exportHighResPng(options: { scale: number; paper?: boolean }): Promise<Uint8Array> {\n const img = await this.renderHighRes(options);\n return encodePng(img.width, img.height, img.data);\n }\n\n async renderHighRes(options: { scale: number; paper?: boolean }): Promise<PixelImage> {\n const target = this.createReplayTarget(options.scale);\n try {\n await replayPainting(this.doc, target, { scale: options.scale });\n await target.wetIdle();\n return target.compositePixels({ paper: options.paper, wet: 'dried' });\n } finally {\n target.dispose();\n }\n }\n\n // --- Wet media -----------------------------------------------------------------\n\n /**\n * Lets wet paint flow with the wall clock: call every frame with `performance.now()` (the\n * bindings do). Steps at the wet speed unless paused; resolves when the frame's steps are done.\n */\n tickWet(now: number): Promise<void> {\n return this.wet.tick(now);\n }\n\n /** Resolves when the wet kernel is as fast as it will get here (`null` when nothing is wet yet). */\n wetReady(): Promise<WetBackendInfo | null> {\n return this.wet.world ? this.wet.world.ready : Promise.resolve(null);\n }\n\n /** Stops the wet simulation's worker threads (call when the editor goes away). */\n dispose(): void {\n this.wet.destroy();\n }\n\n /** Resolves when wet paint is between steps (edits, undo and exports can run). */\n wetIdle(): Promise<void> {\n return this.wet.idle();\n }\n\n private wetLayer(layerId: string | undefined, what: string): PaintPixelLayer {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) throw new PaintEditorError(`There is no active layer to ${what}; pass layerId (list_layers).`);\n const l = this.requireLayer(id);\n if (!isPixelLayer(l)) throw new PaintEditorError(`Layer \"${l.name}\" is a group; ${what} a pixel layer inside it.`);\n return l;\n }\n\n private checkRect(rect: RasterRect | undefined): RasterRect | undefined {\n if (!rect) return undefined;\n const { width, height } = this.doc.canvas;\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) || !(rect.width > 0 && rect.height > 0)) throw new PaintEditorError('rect needs finite x, y and a width and height above 0 (document pixels).');\n const r = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.ceil(rect.width), height: Math.ceil(rect.height) };\n if (r.x >= width || r.y >= height || r.x + r.width <= 0 || r.y + r.height <= 0) throw new PaintEditorError(`rect is outside the ${width}×${height} canvas.`);\n return r;\n }\n\n /** Wet media as panels and tools show it, with each wet layer's state. */\n wetState(): PaintWetStateInfo & { layers: WetLayerInfo[]; backend: WetBackendInfo | null; message: string | null; engine: WetEngineInfo } {\n const st = this.getState().wet;\n const backend = this.wet.backend();\n return { ...st, layers: this.wet.wetLayerIds().map((id) => this.wet.info(id)), backend, message: backend?.message ?? wetSupport().message, engine: this.wet.engineInfo() };\n }\n\n /**\n * Engine B, the WebGPU accelerator for large wet areas: whether it may run, its state (`off`,\n * `not-started`, `starting`, `unavailable`, `standby`, `active`, `failed`), why, and the GPU.\n * The CPU engine stays the authority: drying, saving, export and replay never use the GPU.\n */\n wetEngine(): WetEngineInfo {\n return this.wet.engineInfo();\n }\n\n /**\n * Lets Engine B run (`gpu: 'auto'`, the default) or forces the CPU (`'off'`); `minTiles` is the\n * wet area (live 256 px tiles) at which the GPU takes over. Not an undo step and not saved with\n * the painting (it changes how wet paint is computed on this machine, never the result).\n * Resolves once the GPU has started or turned out unavailable.\n */\n async setWetEngine(options: { gpu?: WetGpuMode; minTiles?: number }): Promise<WetEngineInfo> {\n const gpu = options.gpu ?? this.wet.gpuMode;\n if (!WET_GPU_MODES.includes(gpu)) throw new PaintEditorError(`gpu must be ${WET_GPU_MODES.join(' or ')}, not ${JSON.stringify(gpu)}.`);\n if (options.minTiles !== undefined && !(Number.isInteger(options.minTiles) && options.minTiles >= 1 && options.minTiles <= 1024)) throw new PaintEditorError(`minTiles ${options.minTiles} is out of range; use 1..1024 tiles of 256 px.`);\n this.wet.setGpu(gpu, options.minTiles);\n this.emit();\n const info = await this.wet.gpuReady();\n this.emit();\n return info;\n }\n\n /** One layer's wet settings and state. */\n wetLayerInfo(layerId?: string): WetLayerInfo {\n return this.wet.info(this.wetLayer(layerId, 'read the wet paint of').id);\n }\n\n /**\n * Lets the paint flow for `steps` simulation steps now (or `seconds` at the wet speed): what\n * agents call between strokes. Not an undo step (time passing never is). Returns the steps run\n * and which layers are still wet.\n */\n async advanceWet(options: { steps?: number; seconds?: number }): Promise<{ steps: number; clock: number; stillWet: boolean; layers: WetLayerInfo[] }> {\n const steps = options.steps ?? Math.round((options.seconds ?? 1) * this.wet.speed);\n if (!(Number.isInteger(steps) && steps >= 0 && steps <= 100000)) throw new PaintEditorError(`steps ${steps} is out of range; use 0..100000 (or seconds).`);\n const run = await this.wet.advance(steps);\n this.emit();\n const st = this.wetState();\n return { steps: run, clock: st.clock, stillWet: st.wet, layers: st.layers };\n }\n\n /**\n * Dries wet paint at once: everything settles where it is and the water goes (one undo step).\n * On one layer or all, optionally only inside `rect`. Returns what dried and whether anything is\n * still wet.\n */\n dryPaint(options: { layerId?: string; rect?: RasterRect } = {}): { dried: Array<{ layerId: string; bounds: RasterRect | null }>; stillWet: boolean } {\n this.requireIdle('drying');\n this.requireWetIdle('drying');\n if (options.layerId !== undefined) this.wetLayer(options.layerId, 'dry');\n const rect = this.checkRect(options.rect);\n const ids = options.layerId !== undefined ? this.wet.wetLayerIds().filter((x) => x === options.layerId) : this.wet.wetLayerIds();\n if (!ids.length) return { dried: [], stillWet: this.wet.isWet() };\n const before = this.doc;\n this.beginWetOp();\n const dried = ids.flatMap((id) => this.wet.dry(id, rect));\n this.logWet({ kind: 'dry', layerId: options.layerId, data: rect });\n this.commit('Dry paint', this.withWetEvents(this.doc), before);\n return { dried, stillWet: this.wet.isWet() };\n }\n\n /**\n * Pre-wets the paper (wet-in-wet): fills the paper's water over `rect` (default the canvas) by\n * `amount` 0..1 on a layer's wet layer, so paint laid there spreads softly. One undo step.\n */\n wetPaper(options: { layerId?: string; rect?: RasterRect; amount?: number } = {}): { layerId: string; rect: RasterRect; wet: boolean } {\n this.requireIdle('wetting the paper');\n const l = this.wetLayer(options.layerId, 'wet the paper of');\n if (isLayerLocked(l, 'pixels')) throw new PaintEditorError(`Layer \"${l.name}\" has its pixels locked; unlock it first.`);\n const amount = options.amount ?? 1;\n if (!(amount > 0 && amount <= 1)) throw new PaintEditorError(`amount ${amount} is out of range; use above 0 up to 1.`);\n const rect = this.checkRect(options.rect) ?? { x: 0, y: 0, width: this.doc.canvas.width, height: this.doc.canvas.height };\n if (!this.wet.wetLayerIds().includes(l.id) && this.wet.wetLayerIds().length >= MAX_WET_LANES) throw new PaintEditorError(`At most ${MAX_WET_LANES} layers can hold wet paint at once; dry one first (dry_paint).`);\n const before = this.doc;\n this.beginWetOp();\n const world = this.wet.ensureLane(l.id);\n world.wetPaper(l.id, rect, null, amount);\n this.wet.syncDisplays();\n this.logWet({ kind: 'wet-paper', layerId: l.id, data: { rect, amount } });\n this.commit('Wet paper', this.withWetEvents(this.doc), before);\n return { layerId: l.id, rect, wet: this.wet.isWet(l.id) };\n }\n\n /** Records a settings change as a wet event (not an undo step). */\n private recordWetSetting(e: Omit<WetEvent, 'clock'>): void {\n this.doc = { ...this.doc, wetEvents: [...(this.doc.wetEvents ?? []), { clock: this.wet.world?.clock ?? 0, at: this.doc.strokes.length, ...e }] };\n }\n\n /** Changes a layer's wet settings (viscosity, evaporation, granulation…). Editor settings saved with the document, not an undo step. */\n setWetParams(layerId: string | undefined, update: Partial<WetParams>): WetParams {\n const l = this.wetLayer(layerId, 'change the wet settings of');\n this.requireWetIdle('changing wet settings');\n const params = this.wet.setParams(l.id, update);\n this.recordWetSetting({ kind: 'params', layerId: l.id, data: params });\n this.emit([]);\n return params;\n }\n\n /** Sets a layer's flow map (water pushed in a pattern). Saved with the document, not an undo step. */\n setFlowMap(layerId: string | undefined, update: Partial<WetFlowSpec>): WetFlowSpec {\n const l = this.wetLayer(layerId, 'set the flow map of');\n this.requireWetIdle('changing the flow map');\n const flow = this.wet.setFlow(l.id, update);\n this.recordWetSetting({ kind: 'flow', layerId: l.id, data: flow });\n this.emit([]);\n return flow;\n }\n\n /** Tilts the canvas (water runs downhill; steep tilts make drips). Saved with the document, not an undo step. */\n setCanvasTilt(update: Partial<WetTilt>): WetTilt {\n this.requireWetIdle('tilting the canvas');\n if (update.angle !== undefined && !(update.angle >= 0 && update.angle <= 90)) throw new PaintEditorError(`angle ${update.angle} is out of range; use 0 (flat) .. 90 (upright) degrees.`);\n const tilt = this.wet.setTilt(update);\n this.recordWetSetting({ kind: 'tilt', data: tilt });\n this.emit([]);\n return tilt;\n }\n\n /** Stops (or restarts) time: paused paint stays wet. Editor state. */\n setWetPaused(paused: boolean): boolean {\n this.wet.paused = paused;\n this.emit([]);\n return paused;\n }\n\n /** Simulation steps per second while time runs (1..2000; default 120). Editor state. */\n setWetSpeed(speed: number): number {\n if (!(speed >= 1 && speed <= 2000)) throw new PaintEditorError(`speed ${speed} is out of range; use 1..2000 steps per second.`);\n this.wet.speed = speed;\n this.emit([]);\n return speed;\n }\n\n /** Wet strokes dry as soon as they end (each stroke still one undo step). Editor state. */\n setInstantDry(on: boolean): boolean {\n this.wet.instantDry = on;\n this.emit([]);\n return on;\n }\n\n /** Shows or hides the water overlay (where the water is, view only). */\n setWaterOverlay(visible: boolean): boolean {\n this.wet.overlay = visible;\n this.emit([]);\n return visible;\n }\n\n /** The water overlay's tiles over a document rectangle (RGBA, view only). */\n waterOverlayTiles(rect: RasterRect): Array<{ tx: number; ty: number; data: Uint8ClampedArray }> {\n if (this.wet.busy) return [];\n return this.wet.waterOverlay(rect);\n }\n\n // --- Rendering -----------------------------------------------------------------\n\n private sources() {\n return { width: this.doc.canvas.width, height: this.doc.canvas.height };\n }\n\n /** The layers with the live stroke's surface in place. */\n private currentLayers(): PaintLayer[] {\n const live = this.live;\n if (!live) return this.doc.layers;\n if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;\n const layers = mapLayer(this.doc.layers, live.layerId, (l) => ({ ...(l as PaintPixelLayer), surface: live.surface }));\n this.liveLayers = { key: live.surface, layers };\n return layers;\n }\n\n /** View-only layers between the paper and the paint (the painting guides add the tracing image). */\n protected underlayLayers(): ViewLayer[] {\n const t = this.guides.tracing;\n if (!t || !t.visible) return [];\n return [this.tracingNode(t)];\n }\n\n /** The tracing image resampled onto the canvas (cached until it or the canvas changes). */\n private tracingNode(t: TracingImage): ImageNode {\n const { width, height } = this.doc.canvas;\n const key = this.tracingVersion * 100003 + width * 7 + height;\n if (this.tracingCache?.key === key && this.tracingCache.node.opacity === t.opacity) return this.tracingCache.node;\n const src = this.imagePixels.get(t.src) ?? this.decodeImage(t.src);\n const image = resampleInto(src, t.rect, width, height);\n const node: ImageNode = { type: 'image', id: TRACING_NODE_ID, name: 'Tracing', visible: true, opacity: t.opacity, blend: 'normal', image, version: key + Math.round(t.opacity * 1000) / 1e4 };\n this.tracingCache = { key, node };\n return node;\n }\n\n /** The layers with every wet layer dried where it is (what saving and exporting see). */\n private driedLayers(layers: PaintLayer[] = this.currentLayers()): PaintLayer[] {\n const ids = this.wet.wetLayerIds();\n if (!ids.length) return layers;\n this.requireWetIdle('exporting');\n let out = layers;\n for (const id of ids) {\n const baked = this.wet.bakedSurface(id);\n if (baked && findLayer(out, id)) out = mapLayer(out, id, (l) => ({ ...(l as PaintPixelLayer), surface: baked }));\n }\n return out;\n }\n\n /** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */\n private litLayers(layers: PaintLayer[]): PaintLayer[] {\n const thick = this.doc.thick;\n const live = this.liveThick;\n if (!thick && !live) return layers;\n let out = layers;\n const ids = new Set([...Object.keys(thick ?? {}), ...(live ? [live.layerId] : [])]);\n for (const id of ids) {\n const ch = live?.layerId === id ? live.channels : thick?.[id];\n const l = ch && findLayer(out, id)?.layer;\n if (!ch || !isPixelLayer(l)) continue;\n let cache = this.litCaches.get(id);\n if (!cache) this.litCaches.set(id, (cache = new LitCache()));\n const lit = cache.lit(l.surface, ch, this.lighting);\n if (lit !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: lit }));\n }\n return out;\n }\n\n private renderLayers(options: RenderOptions, view = false): ViewLayer[] {\n let layers: PaintLayer[] = this.litLayers((options.wet ?? 'display') === 'dried' ? this.driedLayers() : this.currentLayers());\n if (options.layerIds) {\n const picked: PaintLayer[] = [];\n for (const id of options.layerIds) picked.push(findLayer(layers, id)?.layer ?? this.requireLayer(id));\n layers = picked.map((l) => ({ ...l, visible: true }) as PaintLayer);\n }\n const under: ViewLayer[] = options.paper === false ? [] : [paperNode(this.doc.paper, this.lighting)];\n if (view) under.push(...this.underlayLayers());\n return [...under, ...layers];\n }\n\n /**\n * Composites the viewport stack (paper, view-only underlays, layers with the live stroke) tile by\n * tile over `rect`, calling `onTile` with each 256 px tile's RGBA (cached: unchanged tiles come back\n * as the same buffer). Pass `takeDirty()` as `dirty`.\n */\n compositeTiles(rect: RasterRect, onTile: (tx: number, ty: number, data: Uint8ClampedArray, changed: boolean) => void, options: { dirty?: RasterRect[] | 'all' } = {}): void {\n const dirty = options.dirty === 'all' ? undefined : options.dirty;\n this.viewCompositor.compositeTiles(this.renderLayers({}, true), rect, this.sources(), onTile, { dirty, checkpointLayerId: this.live?.layerId ?? this.doc.activeLayerId });\n }\n\n /** The canvas as canvas-sized RGBA (paper included by default). */\n compositePixels(options: RenderOptions = {}): PixelImage {\n const { width, height } = this.doc.canvas;\n const data = this.exportCompositor.composite(this.renderLayers({ ...options, wet: options.wet ?? 'dried' }), { x: 0, y: 0, width, height }, this.sources());\n return { width, height, data };\n }\n\n /** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */\n renderPreview(options: RenderOptions & { maxSide?: number } = {}): PixelImage {\n const full = this.compositePixels({ ...options, wet: options.wet ?? 'display' });\n const side = Math.max(1, Math.round(options.maxSide ?? 512));\n const k = Math.min(1, side / Math.max(full.width, full.height));\n if (k >= 1) return full;\n const w = Math.max(1, Math.round(full.width * k));\n const h = Math.max(1, Math.round(full.height * k));\n return { width: w, height: h, data: downsampleRgba(full.data, full.width, full.height, w, h) };\n }\n\n // --- Save, load and export -------------------------------------------------------\n\n /** The document as JSON with inline PNG assets. */\n /** The document as saved: wet paint dried where it is (deterministically); wet settings as data. */\n private savedState(): PaintDocumentState {\n const layers = this.driedLayers(this.doc.layers);\n return layers === this.doc.layers ? this.doc : { ...this.doc, layers };\n }\n\n /** Wet settings saved with the document (`null` when they are all defaults). */\n private wetJSON(): unknown {\n const layers: Record<string, WetLayerSettings> = {};\n for (const [id, set] of this.wet.settings) if (findLayer(this.doc.layers, id)) layers[id] = { params: { ...set.params }, flow: { ...set.flow } };\n const flat = this.wet.tilt.angle === 0;\n if (!Object.keys(layers).length && flat) return undefined;\n return { tilt: { ...this.wet.tilt }, layers };\n }\n\n private readWet(raw: unknown, problems: string[]): void {\n this.wet.reset();\n if (!raw || typeof raw !== 'object') return;\n const r = raw as { tilt?: unknown; layers?: unknown };\n this.wet.setTilt(normalizeTilt((r.tilt as Partial<WetTilt>) ?? {}));\n if (r.layers && typeof r.layers === 'object') {\n for (const [id, v] of Object.entries(r.layers as Record<string, unknown>)) {\n if (!v || typeof v !== 'object') {\n problems.push(`wet.layers[\"${id}\"] is not wet settings; skipped.`);\n continue;\n }\n const x = v as { params?: Partial<WetParams>; flow?: Partial<WetFlowSpec> };\n this.wet.settings.set(id, { params: normalizeWetParams(x.params ?? {}), flow: normalizeFlow(x.flow ?? {}) });\n }\n }\n }\n\n /** Thick paint and light as saved (`layerChannels`, `lighting`), merged into the extensions. */\n private thickExtensions(): PaintDocumentExtensions {\n const ext: PaintDocumentExtensions = { ...this.extensions };\n const channels: Record<string, unknown> = {};\n for (const [id, ch] of Object.entries(this.doc.thick ?? {})) {\n if (!findLayer(this.doc.layers, id)) continue;\n const saved = thickToJSON(ch);\n if (saved) channels[id] = saved;\n }\n if (Object.keys(channels).length) ext.layerChannels = channels;\n else delete ext.layerChannels;\n const stickers: Record<string, Sticker[]> = {};\n for (const [id, list] of Object.entries(this.doc.stickers ?? {})) if (list.length && findLayer(this.doc.layers, id)) stickers[id] = list.map((x) => ({ ...x }));\n if (Object.keys(stickers).length) ext.stickers = stickers;\n else delete ext.stickers;\n if (this.lighting.azimuth !== DEFAULT_LIGHTING.azimuth || this.lighting.elevation !== DEFAULT_LIGHTING.elevation || this.lighting.relief !== DEFAULT_LIGHTING.relief) ext.lighting = { ...this.lighting };\n else delete ext.lighting;\n return ext;\n }\n\n /** Reads `layerChannels` and `lighting` from loaded extensions into the document. */\n private readThick(state: PaintDocumentState, ext: PaintDocumentExtensions, problems: string[]): PaintDocumentState {\n this.lighting = normalizeLighting((ext.lighting as Partial<PaintLighting>) ?? {}, DEFAULT_LIGHTING);\n this.litCaches.clear();\n if (ext.stickers && typeof ext.stickers === 'object') {\n const stickers: Record<string, Sticker[]> = {};\n for (const [id, list] of Object.entries(ext.stickers as Record<string, unknown>)) {\n if (!findLayer(state.layers, id) || !Array.isArray(list)) continue;\n const ok = (list as Sticker[]).filter((x) => x && typeof x.id === 'string' && STAMP_KINDS.includes(x.stamp) && Number.isFinite(x.x) && Number.isFinite(x.y) && x.size > 0);\n if (ok.length !== list.length) problems.push(`Some stickers of layer \"${id}\" could not be read; they were dropped.`);\n stickers[id] = ok.map((x) => ({ ...x }));\n }\n if (Object.keys(stickers).length) state = { ...state, stickers };\n }\n const raw = ext.layerChannels;\n if (!raw || typeof raw !== 'object') return state;\n const thick: Record<string, ThickChannels> = {};\n for (const [id, v] of Object.entries(raw as Record<string, unknown>)) {\n if (!findLayer(state.layers, id)) continue;\n try {\n thick[id] = thickFromJSON(v);\n } catch (e) {\n problems.push(`The thick paint of layer \"${id}\" could not be read (${(e as Error).message}); it loads flat.`);\n }\n }\n return Object.keys(thick).length ? { ...state, thick } : state;\n }\n\n toJSON(options: { strokes?: boolean } = {}): SerializedPaintDocument {\n return serializePaintDocument(this.savedState(), { strokes: options.strokes, extensions: this.thickExtensions(), guides: this.guides, swatches: this.swatches, mixingPad: this.mixingPadJSON(), wet: this.wetJSON() });\n }\n\n /** The document as JSON with each PNG stored in `store`. */\n toJSONExternal(store: PaintAssetStore, options: { strokes?: boolean } = {}): Promise<SerializedPaintDocument> {\n return serializePaintDocumentExternal(this.savedState(), store, { strokes: options.strokes, extensions: this.thickExtensions(), guides: this.guides, swatches: this.swatches, mixingPad: this.mixingPadJSON(), wet: this.wetJSON() });\n }\n\n private adopt(loaded: { state: PaintDocumentState; extensions: PaintDocumentExtensions; problems: string[]; guides?: unknown; swatches?: unknown; mixingPad?: unknown; wet?: unknown }): string[] {\n this.extensions = loaded.extensions;\n this.readWet(loaded.wet, loaded.problems);\n this.readMixingPad(loaded.mixingPad, loaded.problems);\n this.readGuides(loaded.guides, loaded.state, loaded.problems);\n if (Array.isArray(loaded.swatches)) this.swatches = loaded.swatches.map(readSwatch).filter((x): x is Swatch => x !== null);\n for (const s of loaded.state.strokes) {\n const n = Number(/^s(\\d+)$/.exec(s.id)?.[1] ?? 0);\n if (n >= this.strokeCounter) this.strokeCounter = n + 1;\n }\n this.undoStack = [];\n this.redoStack = [];\n this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);\n delete this.extensions.layerChannels;\n delete this.extensions.lighting;\n delete this.extensions.stickers;\n this.viewCompositor.clear();\n this.emit();\n return loaded.problems;\n }\n\n /** Opens a saved document (clears the history). Returns what could not be read. */\n loadJSON(raw: unknown): string[] {\n this.requireIdle('opening a document');\n try {\n return this.adopt(deserializePaintDocument(raw));\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** Opens a saved document whose assets may live in `store`. */\n async loadJSONAsync(raw: unknown, store?: PaintAssetStore): Promise<string[]> {\n this.requireIdle('opening a document');\n try {\n return this.adopt(await deserializePaintDocumentAsync(raw, store));\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** The canvas as PNG bytes (paper included by default; `scale` below 1 shrinks it). */\n exportPng(options: RenderOptions & { scale?: number } = {}): Uint8Array {\n const img = this.scaled(options);\n return encodePng(img.width, img.height, img.data);\n }\n\n private scaled(options: RenderOptions & { scale?: number }): PixelImage {\n const full = this.compositePixels(options);\n const s = options.scale ?? 1;\n if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);\n if (s === 1) return full;\n const w = Math.max(1, Math.round(full.width * s));\n const h = Math.max(1, Math.round(full.height * s));\n return { width: w, height: h, data: downsampleRgba(full.data, full.width, full.height, w, h) };\n }\n\n /**\n * The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.\n * JPEG and WebP need a canvas (a browser or Worker) or the `encodeImage` option.\n */\n async exportImage(options: RenderOptions & { format?: ImageFormat; quality?: number; scale?: number } = {}): Promise<Uint8Array> {\n const format = options.format ?? 'image/png';\n if (format === 'image/png') return this.exportPng(options);\n if (format !== 'image/jpeg' && format !== 'image/webp') throw new PaintEditorError(`format ${JSON.stringify(format)} is not supported; use image/png, image/jpeg or image/webp.`);\n const quality = options.quality ?? 0.92;\n if (!(quality >= 0 && quality <= 1)) throw new PaintEditorError(`quality ${quality} is out of range; use 0..1.`);\n const img = this.scaled({ ...options, paper: format === 'image/jpeg' ? true : options.paper });\n const encode = this.encodeImage ?? defaultImageEncoder();\n if (!encode) throw new PaintEditorError(`${format} needs a canvas to encode and there is none here; pass encodeImage to the controller, or export PNG.`);\n return encode(img, format, quality);\n }\n\n /**\n * The document as an 8-bit RGB PSD through the shared PSD writer: every layer and group, with\n * the paper as a locked \"Paper\" pixel layer at the bottom (it is generated, so PSD gets its\n * pixels), and the composite as the merged image.\n */\n exportPsd(): PsdExportResult {\n const dried = this.litLayers(this.driedLayers(this.doc.layers));\n const { width, height, dpi } = this.doc.canvas;\n const paperPixels = this.compositePixels({ layerIds: [], paper: true });\n const paperLayer: PaintPixelLayer = {\n type: 'pixel',\n id: 'paper',\n name: 'Paper',\n visible: true,\n opacity: 1,\n blend: 'normal',\n locks: { all: true },\n surface: surfaceFromPixels(paperPixels, 4),\n offset: { x: 0, y: 0 },\n };\n const merged = this.compositePixels({ paper: true }).data;\n // PSD has no pigment or painterly modes: write each as its nearest standard mode and say so.\n const changed = new Map<BlendMode, string[]>();\n const toStandard = (layers: readonly PaintLayer[]): PaintLayer[] =>\n layers.map((l) => {\n const blend = l.blend === 'pass-through' ? l.blend : standardBlendMode(l.blend as BlendMode);\n if (blend !== l.blend) changed.set(l.blend as BlendMode, [...(changed.get(l.blend as BlendMode) ?? []), l.name]);\n const next = blend === l.blend ? l : ({ ...l, blend } as PaintLayer);\n return next.type === 'group' ? { ...next, children: toStandard(next.children) } : next;\n });\n const layers = toStandard(dried);\n const result = exportPsd({ width, height, layers: [paperLayer, ...layers] as unknown as ImageLayer[], merged, resolution: dpi });\n for (const [mode, names] of changed) {\n const label = BLEND_MODES.find((m) => m.id === mode)?.label ?? mode;\n const std = BLEND_MODES.find((m) => m.id === standardBlendMode(mode))?.label ?? standardBlendMode(mode);\n const list = names.map((n) => `\"${n}\"`).join(', ');\n const one = names.length === 1;\n result.warnings.push(`${label} blending has no PSD equivalent; ${list} ${one ? 'was' : 'were'} written as ${std}, the nearest Photoshop mode, so ${one ? 'it looks' : 'they look'} different in Photoshop (the merged image is exact).`);\n }\n return result;\n }\n\n /** `render_preview` as a base64 PNG. */\n previewPngBase64(options: RenderOptions & { maxSide?: number } = {}): { data: string; width: number; height: number } {\n const img = this.renderPreview(options);\n return { data: bytesToBase64(encodePng(img.width, img.height, img.data)), width: img.width, height: img.height };\n }\n\n // --- Guides --------------------------------------------------------------------\n\n private setGuides(next: PaintGuides): PaintGuides {\n this.guides = next;\n this.emit('all');\n return next;\n }\n\n /** Mirror and kaleidoscope painting: every new stroke paints its copies too (one stroke, one undo step). */\n setSymmetry(update: Partial<SymmetryGuide>): SymmetryGuide {\n const symmetry = normalizeSymmetry(update, this.guides.symmetry);\n this.setGuides({ ...this.guides, symmetry });\n return symmetry;\n }\n\n /** Perspective guides with one to three vanishing points that pointer strokes snap to. */\n setPerspective(update: Partial<PerspectiveGuide>): PerspectiveGuide {\n let perspective: PerspectiveGuide;\n try {\n perspective = normalizePerspective(update, this.guides.perspective);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.setGuides({ ...this.guides, perspective });\n return perspective;\n }\n\n /** An image from a PNG (base64 or data URL) or from RGBA pixels, stored as a PNG data URL. */\n private importImage(input: { png?: string; pixels?: PixelImage }, what: string): { src: string; pixels: PixelImage } {\n let pixels: PixelImage;\n if (input.pixels) {\n const { width, height, data } = input.pixels;\n if (!(width > 0 && height > 0 && Number.isInteger(width) && Number.isInteger(height)) || data.length !== width * height * 4) {\n throw new PaintEditorError(`${what}: pixels must be width × height × 4 bytes of RGBA.`);\n }\n pixels = { width, height, data: new Uint8ClampedArray(data) };\n } else if (typeof input.png === 'string' && input.png.trim()) {\n pixels = this.decodeImage(input.png.trim().startsWith('data:') ? input.png.trim() : `data:image/png;base64,${input.png.trim()}`, what);\n } else throw new PaintEditorError(`${what}: pass png (a PNG as base64 or a data URL) or pixels.`);\n if (pixels.width > MAX_IMAGE_SIDE || pixels.height > MAX_IMAGE_SIDE) throw new PaintEditorError(`${what} is ${pixels.width}×${pixels.height}; the limit is ${MAX_IMAGE_SIDE} px per side.`);\n const src = pngDataUrl(pixels.width, pixels.height, pixels.data);\n this.imagePixels.set(src, pixels);\n return { src, pixels };\n }\n\n private decodeImage(src: string, what = 'Image'): PixelImage {\n const cached = this.imagePixels.get(src);\n if (cached) return cached;\n const parsed = parseDataUrl(src);\n if (!parsed) throw new PaintEditorError(`${what} is not a data URL or base64 PNG.`);\n try {\n const png = decodePng(parsed.bytes);\n const px = { width: png.width, height: png.height, data: png.data };\n this.imagePixels.set(src, px);\n return px;\n } catch (e) {\n throw new PaintEditorError(`${what} could not be decoded as a PNG (${(e as Error).message}); decode other formats in the browser and pass pixels.`);\n }\n }\n\n /**\n * Puts an image under the paint to trace over: view only, never in exports. Placed to fit the\n * canvas unless `rect` is given; `sampleColors` makes strokes take their colour from it.\n */\n setTracingImage(options: { png?: string; pixels?: PixelImage; rect?: TracingImage['rect']; opacity?: number; sampleColors?: boolean }): TracingImage {\n const { src, pixels } = this.importImage(options, 'The tracing image');\n const { width, height } = this.doc.canvas;\n const k = Math.min(width / pixels.width, height / pixels.height);\n const fit = { x: (width - pixels.width * k) / 2, y: (height - pixels.height * k) / 2, width: pixels.width * k, height: pixels.height * k };\n const tracing: TracingImage = { src, width: pixels.width, height: pixels.height, rect: fit, opacity: 0.35, visible: true, sampleColors: false };\n return this.updateTracing({ ...options, rect: options.rect ?? fit }, tracing);\n }\n\n /** Changes the tracing image's opacity, visibility, placement or colour sampling. */\n updateTracing(update: Partial<Pick<TracingImage, 'opacity' | 'visible' | 'sampleColors' | 'rect'>>, base = this.guides.tracing): TracingImage {\n if (!base) throw new PaintEditorError('There is no tracing image; set one with set_tracing_image first.');\n if (update.opacity !== undefined && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);\n if (update.rect && !(update.rect.width > 0 && update.rect.height > 0)) throw new PaintEditorError('rect needs a width and height above 0.');\n const tracing: TracingImage = {\n ...base,\n ...(update.opacity !== undefined ? { opacity: update.opacity } : {}),\n ...(update.visible !== undefined ? { visible: update.visible } : {}),\n ...(update.sampleColors !== undefined ? { sampleColors: update.sampleColors } : {}),\n ...(update.rect ? { rect: { ...update.rect } } : {}),\n };\n if (tracing.src !== base.src || update.rect) this.tracingVersion++;\n if (base !== this.guides.tracing) this.tracingVersion++;\n this.setGuides({ ...this.guides, tracing });\n return tracing;\n }\n\n clearTracingImage(): void {\n this.tracingVersion++;\n this.setGuides({ ...this.guides, tracing: null });\n }\n\n /** The tracing image's colour at a document pixel, or `null` outside it or where it is clear. */\n sampleTracingColor(x: number, y: number): string | null {\n const t = this.guides.tracing;\n if (!t) return null;\n const src = this.decodeImage(t.src);\n const u = Math.floor(((x - t.rect.x) / t.rect.width) * src.width);\n const v = Math.floor(((y - t.rect.y) / t.rect.height) * src.height);\n if (u < 0 || v < 0 || u >= src.width || v >= src.height) return null;\n const i = (v * src.width + u) * 4;\n if (src.data[i + 3] === 0) return null;\n return rgbToHex(src.data[i], src.data[i + 1], src.data[i + 2]);\n }\n\n /** Adds a floating reference image to look at and pick colours from. */\n addReferenceImage(options: { png?: string; pixels?: PixelImage; name?: string; x?: number; y?: number; displayWidth?: number }): ReferenceImage {\n const { src, pixels } = this.importImage(options, 'The reference image');\n const ref: ReferenceImage = {\n id: `ref${this.nextReferenceId++}`,\n name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,\n src,\n width: pixels.width,\n height: pixels.height,\n x: options.x ?? 16 + this.guides.references.length * 24,\n y: options.y ?? 16 + this.guides.references.length * 24,\n displayWidth: options.displayWidth ?? Math.min(240, pixels.width),\n visible: true,\n };\n this.setGuides({ ...this.guides, references: [...this.guides.references, ref] });\n return ref;\n }\n\n private requireReference(id: string): ReferenceImage {\n const r = this.guides.references.find((x) => x.id === id);\n if (!r) throw new PaintEditorError(`Unknown reference image \"${id}\". Call list_reference_images for the ids.`);\n return r;\n }\n\n /** Moves, resizes, renames, shows or hides a reference image. */\n updateReferenceImage(id: string, update: Partial<Pick<ReferenceImage, 'name' | 'x' | 'y' | 'displayWidth' | 'visible'>>): ReferenceImage {\n const r = this.requireReference(id);\n if (update.displayWidth !== undefined && !(update.displayWidth >= 16 && update.displayWidth <= 4096)) throw new PaintEditorError(`displayWidth ${update.displayWidth} is out of range; use 16..4096.`);\n const next: ReferenceImage = { ...r, ...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== undefined)) };\n this.setGuides({ ...this.guides, references: this.guides.references.map((x) => (x.id === id ? next : x)) });\n return next;\n }\n\n removeReferenceImage(id: string): void {\n this.requireReference(id);\n this.setGuides({ ...this.guides, references: this.guides.references.filter((x) => x.id !== id) });\n }\n\n /** A reference image's pixels (decoded once). */\n referencePixels(id: string): PixelImage {\n return this.decodeImage(this.requireReference(id).src);\n }\n\n /** The colour of a reference image at its pixel (x, y). */\n pickReferenceColor(id: string, x: number, y: number): string {\n const r = this.requireReference(id);\n const px = this.decodeImage(r.src);\n const u = Math.floor(x);\n const v = Math.floor(y);\n if (!(u >= 0 && v >= 0 && u < px.width && v < px.height)) throw new PaintEditorError(`(${x}, ${y}) is outside reference \"${r.name}\" (${px.width}×${px.height}); use its own pixel coordinates.`);\n const i = (v * px.width + u) * 4;\n return rgbToHex(px.data[i], px.data[i + 1], px.data[i + 2]);\n }\n\n private readGuides(raw: unknown, state: PaintDocumentState, problems: string[]): void {\n const base = defaultGuides(state.canvas);\n this.guides = base;\n this.tracingVersion++;\n if (!raw || typeof raw !== 'object') return;\n const r = raw as Record<string, unknown>;\n const guides: PaintGuides = { ...base };\n guides.symmetry = normalizeSymmetry((r.symmetry as Record<string, unknown>) ?? {}, base.symmetry);\n try {\n guides.perspective = normalizePerspective((r.perspective as Record<string, unknown>) ?? {}, base.perspective);\n } catch (e) {\n problems.push(`Perspective guide: ${(e as Error).message}`);\n }\n const t = r.tracing as TracingImage | null | undefined;\n if (t && typeof t.src === 'string') {\n try {\n const px = this.decodeImage(t.src, 'The tracing image');\n guides.tracing = {\n src: t.src,\n width: px.width,\n height: px.height,\n rect: t.rect && t.rect.width > 0 && t.rect.height > 0 ? { ...t.rect } : { x: 0, y: 0, width: state.canvas.width, height: state.canvas.height },\n opacity: typeof t.opacity === 'number' ? Math.min(1, Math.max(0, t.opacity)) : 0.35,\n visible: t.visible !== false,\n sampleColors: t.sampleColors === true,\n };\n } catch (e) {\n problems.push((e as Error).message);\n }\n }\n if (Array.isArray(r.references)) {\n for (const ref of r.references as ReferenceImage[]) {\n if (!ref || typeof ref.src !== 'string' || typeof ref.id !== 'string') continue;\n try {\n const px = this.decodeImage(ref.src, `Reference \"${ref.name}\"`);\n guides.references.push({ id: ref.id, name: String(ref.name ?? ref.id), src: ref.src, width: px.width, height: px.height, x: Number(ref.x) || 0, y: Number(ref.y) || 0, displayWidth: Number(ref.displayWidth) || 240, visible: ref.visible !== false });\n const n = Number(/^ref(\\d+)$/.exec(ref.id)?.[1] ?? 0);\n if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;\n } catch (e) {\n problems.push((e as Error).message);\n }\n }\n }\n if (Array.isArray(r.stencils)) {\n guides.stencils = [];\n (r.stencils as Stencil[]).forEach((raw, i) => {\n try {\n const id = typeof raw?.id === 'string' && /^stencil\\d+$/.test(raw.id) ? raw.id : `stencil${this.nextStencilId}`;\n guides.stencils.push(normalizeStencil(raw, id));\n const n = Number(id.slice(7));\n if (n >= this.nextStencilId) this.nextStencilId = n + 1;\n } catch (e) {\n problems.push(`Stencil ${i + 1}: ${(e as Error).message}`);\n }\n });\n }\n this.guides = guides;\n }\n\n // --- Stickers --------------------------------------------------------------------\n\n /** The document's `stickers` without these layers (as a spread: `{}` when unchanged). */\n private stickersWithout(ids: readonly string[]): { stickers?: Record<string, Sticker[]> } {\n const t = this.doc.stickers;\n if (!t || !ids.some((id) => t[id])) return {};\n const out = { ...t };\n for (const id of ids) delete out[id];\n return { stickers: out };\n }\n\n /** The next free sticker number (ids `st<n>` are unique across the document). */\n private nextStickerNumber(doc: PaintDocumentState = this.doc): number {\n let n = 0;\n for (const list of Object.values(doc.stickers ?? {})) for (const st of list) n = Math.max(n, Number(/^st(\\d+)$/.exec(st.id)?.[1] ?? 0));\n return n + 1;\n }\n\n private stickerLayer(layerId: string | undefined): { layer: PaintPixelLayer; stickers: Sticker[] } {\n const l = this.wetLayer(layerId, 'edit the stickers of');\n const stickers = this.doc.stickers?.[l.id];\n if (!stickers) throw new PaintEditorError(`Layer \"${l.name}\" has no stickers; spray some with a sticker brush (list_brushes category sticker).`);\n return { layer: l, stickers };\n }\n\n /** A layer's stickers, bottom first. */\n listStickers(layerId?: string): Sticker[] {\n const id = layerId ?? this.doc.activeLayerId;\n if (!id) return [];\n this.requireLayer(id);\n return (this.doc.stickers?.[id] ?? []).map((s) => ({ ...s }));\n }\n\n /** Replaces a layer's stickers and redraws its pixels from them (one undo step). */\n private commitStickers(label: string, layer: PaintPixelLayer, stickers: Sticker[]): void {\n if (isLayerLocked(layer, 'pixels')) throw new PaintEditorError(`Layer \"${layer.name}\" has its pixels locked; unlock it first.`);\n const surface = stickerSurface(stickers);\n const doc = { ...this.doc, layers: mapLayer(this.doc.layers, layer.id, () => ({ ...layer, surface, offset: { x: 0, y: 0 } })), stickers: { ...(this.doc.stickers ?? {}), [layer.id]: stickers } };\n this.commit(label, this.withPaintEvent(doc, { kind: 'stickers', layerId: layer.id, data: { stickers: stickers.map((s) => ({ ...s })) } }));\n }\n\n /** Moves, turns, resizes, recolours or restamps one sticker (one undo step). Returns it. */\n updateSticker(layerId: string | undefined, id: string, update: Partial<Omit<Sticker, 'id'>>): Sticker {\n const { layer, stickers } = this.stickerLayer(layerId);\n const i = stickers.findIndex((s) => s.id === id);\n if (i < 0) throw new PaintEditorError(`Layer \"${layer.name}\" has no sticker \"${id}\". Call list_stickers for the ids.`);\n let u: Partial<Omit<Sticker, 'id'>>;\n try {\n u = normalizeStickerUpdate(update);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n const next = { ...stickers[i], ...u };\n this.commitStickers('Edit sticker', layer, stickers.map((s, j) => (j === i ? next : s)));\n return { ...next };\n }\n\n /** Brings stickers to the top (`front`) or bottom of their layer (one undo step). */\n arrangeStickers(layerId: string | undefined, ids: readonly string[], to: 'front' | 'back'): string[] {\n const { layer, stickers } = this.stickerLayer(layerId);\n for (const id of ids) if (!stickers.some((s) => s.id === id)) throw new PaintEditorError(`Layer \"${layer.name}\" has no sticker \"${id}\". Call list_stickers for the ids.`);\n const picked = stickers.filter((s) => ids.includes(s.id));\n const rest = stickers.filter((s) => !ids.includes(s.id));\n const next = to === 'front' ? [...rest, ...picked] : [...picked, ...rest];\n this.commitStickers('Arrange stickers', layer, next);\n return next.map((s) => s.id);\n }\n\n /** Deletes stickers (`ids`, or all of them on the layer) (one undo step). Returns the ids removed. */\n removeStickers(layerId: string | undefined, ids?: readonly string[]): string[] {\n const { layer, stickers } = this.stickerLayer(layerId);\n if (ids) for (const id of ids) if (!stickers.some((s) => s.id === id)) throw new PaintEditorError(`Layer \"${layer.name}\" has no sticker \"${id}\". Call list_stickers for the ids.`);\n const removed = ids ? [...ids] : stickers.map((s) => s.id);\n this.commitStickers('Remove stickers', layer, stickers.filter((s) => !removed.includes(s.id)));\n return removed;\n }\n\n /** Sets a layer's stickers (replays). */\n replaceStickers(layerId: string, stickers: readonly Sticker[]): void {\n const l = this.wetLayer(layerId, 'set the stickers of');\n this.commitStickers('Stickers', l, stickers.map((s) => ({ ...s })));\n }\n\n // --- Stencils --------------------------------------------------------------------\n\n private stencilCache: { key: unknown; size: string; mask: { width: number; height: number; data: Uint8Array } | null } | null = null;\n private nextStencilId = 1;\n\n /** The mask stencils' coverage over the canvas (cached), or `null` when none masks. */\n private stencilSelection(stencils: readonly Stencil[] = this.guides.stencils): { width: number; height: number; data: Uint8Array } | null {\n const { width, height } = this.doc.canvas;\n const size = `${width}x${height}`;\n if (this.stencilCache?.key === stencils && this.stencilCache.size === size) return this.stencilCache.mask;\n const mask = stencilMask(stencils, width, height);\n this.stencilCache = { key: stencils, size, mask };\n return mask;\n }\n\n /** The stencils. */\n listStencils(): Stencil[] {\n return this.guides.stencils.map((s) => ({ ...s }));\n }\n\n /** Lays a stencil on the canvas (a ruler or arc to follow, a straight edge or shape to mask). Editor state saved with the document, not an undo step. */\n addStencil(input: StencilInput): Stencil {\n let stencil: Stencil;\n try {\n stencil = normalizeStencil(input, `stencil${this.nextStencilId++}`);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.setGuides({ ...this.guides, stencils: [...this.guides.stencils, stencil] });\n return { ...stencil };\n }\n\n private requireStencil(id: string): Stencil {\n const s = this.guides.stencils.find((x) => x.id === id);\n if (!s) throw new PaintEditorError(`Unknown stencil \"${id}\". Call get_guides for the stencil ids.`);\n return s;\n }\n\n /** Moves, reshapes, switches (guide / mask, invert) or hides a stencil. */\n updateStencil(id: string, update: Partial<Omit<Stencil, 'id' | 'kind'>>): Stencil {\n const base = this.requireStencil(id);\n let next: Stencil;\n try {\n next = normalizeStencil({ ...update, kind: base.kind }, id, base);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.setGuides({ ...this.guides, stencils: this.guides.stencils.map((x) => (x.id === id ? next : x)) });\n return { ...next };\n }\n\n /** Takes stencils away (`ids`, or all of them). Returns the ids removed. */\n removeStencils(ids?: readonly string[]): string[] {\n if (ids) for (const id of ids) this.requireStencil(id);\n const removed = ids ? [...ids] : this.guides.stencils.map((s) => s.id);\n this.setGuides({ ...this.guides, stencils: this.guides.stencils.filter((s) => !removed.includes(s.id)) });\n return removed;\n }\n\n // --- Swatches and harmonies ------------------------------------------------------\n\n /** Colour harmonies around a colour (default the paint colour) on the painter's RYB wheel or the RGB wheel. */\n harmony(kind: HarmonyKind, options: { color?: string; wheel?: ColorWheel; spread?: number } = {}): HarmonyColor[] {\n try {\n return colorHarmony(options.color === undefined ? this.color : parseColor(options.color), kind, options);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** Adds a swatch: a colour, or a paint source (a gradient swatch holds its gradient). */\n addSwatch(color: string | PaintSource, name?: string): Swatch[] {\n this.swatches = [...this.swatches, this.makeSwatch(color, name)];\n this.emit([]);\n return this.swatches;\n }\n\n private makeSwatch(color: string | PaintSource, name?: string): Swatch {\n const p = typeof color === 'string' ? ({ type: 'color', color: parseColor(color) } as PaintSource) : this.readStrokePaint(color);\n const out: Swatch = { color: paintSourceColor(p, { paint: this.color, secondary: this.secondaryColor }) };\n if (name?.trim()) out.name = name.trim();\n if (p.type === 'gradient') out.paint = p;\n return out;\n }\n\n removeSwatch(index: number): Swatch[] {\n if (!Number.isInteger(index) || index < 0 || index >= this.swatches.length) throw new PaintEditorError(`index ${index} is out of range; there are ${this.swatches.length} swatches (0-based).`);\n this.swatches = this.swatches.filter((_, i) => i !== index);\n this.emit([]);\n return this.swatches;\n }\n\n setSwatches(swatches: readonly Swatch[]): Swatch[] {\n this.swatches = swatches.map((x) => this.makeSwatch(x.paint ?? x.color, x.name));\n this.emit([]);\n return this.swatches;\n }\n\n // --- Mixing pad ------------------------------------------------------------------\n\n get mixingPad(): MixingPad {\n return this.pad;\n }\n\n /** Paints (`paint`) or smears with the palette knife (`knife`) on the mixing pad, in pad pixels. */\n mixingPadStroke(points: readonly PenPoint[], options: { tool?: MixingPadTool; color?: string; size?: number } = {}): { bounds: RasterRect | null } {\n const color = options.color === undefined ? this.color : parseColor(options.color);\n if (options.size !== undefined && !(options.size >= 1 && options.size <= 200)) throw new PaintEditorError(`size ${options.size} is out of range; use 1..200 px.`);\n try {\n const r = this.pad.stroke(points, { tool: options.tool ?? 'paint', color, size: options.size });\n this.emit([]);\n return r;\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n }\n\n /** Picks the mixed colour on the pad (averaged over `radius` px); with `setColor` it becomes the paint colour. */\n mixingPadPick(x: number, y: number, options: { radius?: number; setColor?: boolean } = {}): string | null {\n let c: string | null;\n try {\n c = this.pad.pick(x, y, options.radius);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n if (c && options.setColor) this.setColor(c);\n return c;\n }\n\n clearMixingPad(): void {\n this.pad.clear();\n this.emit([]);\n }\n\n /**\n * Mixes paints on the pad like a painter: blobs in proportion to their `parts`, knife passes,\n * and a fully mixed core of the Kubelka-Munk mix (see `MixingPad.mix`). With `setColor` the\n * mix becomes the paint colour. Returns the mixed colour and the pile's bounds.\n */\n mixOnPad(paints: readonly { color: string; parts: number }[], options: PadMixOptions & { setColor?: boolean } = {}): { color: string; bounds: RasterRect } {\n const parsed = (paints ?? []).map((p, i) => ({ color: parseColor(p?.color, `paints[${i}].color`), parts: p?.parts }));\n let r: { color: string | null; bounds: RasterRect };\n try {\n r = this.pad.mix(parsed, options);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n const color = r.color ?? parsed[0].color;\n this.emit([]);\n if (options.setColor) this.setColor(color);\n return { color, bounds: r.bounds };\n }\n\n /** The mixing pad as a PNG data URL (with alpha). */\n mixingPadPng(): string {\n return this.pad.toPng();\n }\n\n /** Replaces the mixing pad with a PNG (data URL or base64, 360 × 220). */\n setMixingPadPng(png: string): void {\n try {\n this.pad.fromPng(png);\n } catch (e) {\n throw new PaintEditorError((e as Error).message);\n }\n this.emit([]);\n }\n\n /** The mixing pads kept by name. */\n listMixingPads(): SavedMixingPadInfo[] {\n return this.savedPads.map(({ id, name }) => ({ id, name }));\n }\n\n /** Keeps the mixing pad as it is now under `name` (saved with the document). Returns its id. */\n saveMixingPad(name?: string): SavedMixingPadInfo {\n if (this.pad.empty) throw new PaintEditorError('The mixing pad is empty; put paint on it (mixing_pad_stroke or mix_on_pad) before saving it.');\n const label = (typeof name === 'string' && name.trim() ? name.trim() : `Pad ${this.nextPadId}`).slice(0, 80);\n const pad: SavedMixingPad = { id: `pad${this.nextPadId++}`, name: label, png: this.pad.toPng() };\n this.savedPads.push(pad);\n this.emit([]);\n return { id: pad.id, name: pad.name };\n }\n\n private requirePad(id: string): SavedMixingPad {\n const p = this.savedPads.find((x) => x.id === id);\n if (!p) throw new PaintEditorError(`Unknown mixing pad \"${id}\". Call list_mixing_pads for the ids.`);\n return p;\n }\n\n /** Puts a kept pad back on the mixing pad (replacing what is there). */\n loadMixingPad(id: string): SavedMixingPadInfo {\n const p = this.requirePad(id);\n this.setMixingPadPng(p.png);\n return { id: p.id, name: p.name };\n }\n\n /** Forgets a kept pad. */\n removeMixingPad(id: string): SavedMixingPadInfo {\n const p = this.requirePad(id);\n this.savedPads = this.savedPads.filter((x) => x !== p);\n this.emit([]);\n return { id: p.id, name: p.name };\n }\n\n private mixingPadJSON(): { current?: string; saved: SavedMixingPad[] } | undefined {\n if (this.pad.empty && !this.savedPads.length) return undefined;\n return { ...(this.pad.empty ? {} : { current: this.pad.toPng() }), saved: this.savedPads.map((p) => ({ ...p })) };\n }\n\n private readMixingPad(raw: unknown, problems: string[]): void {\n this.savedPads = [];\n this.nextPadId = 1;\n this.pad.clear();\n if (raw === undefined || raw === null) return;\n const r = raw as { current?: unknown; saved?: unknown };\n if (typeof r.current === 'string') {\n try {\n this.pad.fromPng(r.current);\n } catch (e) {\n problems.push(`The mixing pad could not be read (${(e as Error).message}); it starts empty.`);\n }\n }\n if (Array.isArray(r.saved)) {\n r.saved.forEach((p, i) => {\n const x = p as Partial<SavedMixingPad>;\n if (!x || typeof x.png !== 'string' || typeof x.name !== 'string') {\n problems.push(`mixingPad.saved[${i}] is not a saved pad; skipped.`);\n return;\n }\n const id = typeof x.id === 'string' && /^pad\\d+$/.test(x.id) ? x.id : `pad${this.nextPadId}`;\n const n = Number(id.slice(3));\n if (n >= this.nextPadId) this.nextPadId = n + 1;\n this.savedPads.push({ id, name: x.name, png: x.png });\n });\n }\n }\n\n /** A layer's tile surface (live while stroking), for thumbnails. */\n layerSurface(id: string): TiledSurface | null {\n const l = findLayer(this.currentLayers(), id)?.layer;\n return isPixelLayer(l) ? l.surface : null;\n }\n}\n\nexport type { Tile };\n\n/** Largest tracing or reference image side, in pixels. */\nexport const MAX_IMAGE_SIDE = 8192;\n\n/** `src` drawn into `rect` of a `width × height` canvas (bilinear), canvas-sized RGBA. */\nfunction resampleInto(src: PixelImage, rect: { x: number; y: number; width: number; height: number }, width: number, height: number): PixelImage {\n const out = new Uint8ClampedArray(width * height * 4);\n const x0 = Math.max(0, Math.floor(rect.x));\n const y0 = Math.max(0, Math.floor(rect.y));\n const x1 = Math.min(width, Math.ceil(rect.x + rect.width));\n const y1 = Math.min(height, Math.ceil(rect.y + rect.height));\n const sx = src.width / rect.width;\n const sy = src.height / rect.height;\n const d = src.data;\n for (let y = y0; y < y1; y++) {\n const v = Math.min(src.height - 1, Math.max(0, (y + 0.5 - rect.y) * sy - 0.5));\n const j0 = Math.floor(v);\n const j1 = Math.min(src.height - 1, j0 + 1);\n const fy = v - j0;\n for (let x = x0; x < x1; x++) {\n const u = (x + 0.5 - rect.x) * sx - 0.5;\n if (u < -0.5 || u > src.width - 0.5 || (y + 0.5 - rect.y) * sy < 0 || (y + 0.5 - rect.y) * sy > src.height) continue;\n const uc = Math.min(src.width - 1, Math.max(0, u));\n const i0 = Math.floor(uc);\n const i1 = Math.min(src.width - 1, i0 + 1);\n const fx = uc - i0;\n const o = (y * width + x) * 4;\n for (let c = 0; c < 4; c++) {\n const a = d[(j0 * src.width + i0) * 4 + c] + (d[(j0 * src.width + i1) * 4 + c] - d[(j0 * src.width + i0) * 4 + c]) * fx;\n const b = d[(j1 * src.width + i0) * 4 + c] + (d[(j1 * src.width + i1) * 4 + c] - d[(j1 * src.width + i0) * 4 + c]) * fx;\n out[o + c] = a + (b - a) * fy;\n }\n }\n }\n return { width, height, data: out };\n}\n","// Agent \"performances\" on the paint canvas, after the chart and diagram directors (see\n// `animation/presence.ts`): a director acts each command out with the agent's cursor in document\n// pixels, so a stroke is painted live along its path (the canvas updates as the dabs land) and\n// commits as one undo step, a fill is rubbed across the sheet, and a replay starts from the strokes\n// it copies. The clock is injectable so performances are repeatable in tests.\nimport {\n PerformanceQueue,\n PresenceStore,\n realPresenceClock,\n tweenPresenceCursor,\n type PresenceActor,\n type PresenceActorInfo,\n type PresenceClock,\n type PresenceState,\n} from '../animation/presence';\nimport type { RasterRect } from '../raster/surface';\nimport type { FillLayerOptions, PaintEditorController, PaintStrokeInit, ReplayOptions } from './controller';\nimport type { PenPoint } from './engine';\nimport type { PaintStrokeResult } from './stroke';\n\nexport type PaintPresenceActor = PresenceActor;\nexport type PaintPresenceState = PresenceState;\n\n/** Cursor positions (document pixels) and notes of agents painting, for an overlay. */\nexport class PaintPresence extends PresenceStore {}\n\n/** What an agent asks for. Each command that paints commits as one undo step. */\nexport type PaintAgentCommand =\n | { kind: 'stroke'; stroke: PaintStrokeInit & { points: readonly PenPoint[] }; note?: string }\n | { kind: 'fill'; fill: FillLayerOptions; note?: string }\n | { kind: 'replay'; replay: ReplayOptions; note?: string }\n | { kind: 'point'; at: { x: number; y: number }; note?: string }\n | { kind: 'say'; text: string };\n\nexport type PaintPerformanceResult =\n | { ok: true; command: PaintAgentCommand; result?: unknown }\n | { ok: false; command: PaintAgentCommand; message: string };\n\nexport interface PaintDirectorOptions {\n clock?: PresenceClock;\n /** 1 = natural speed; 2 = twice as fast. */\n speed?: number;\n /** Hide each actor's cursor this long after its last command (ms, default 1500; 0 keeps it). */\n linger?: number;\n /** Longest a stroke takes to paint at speed 1 (ms, default 1800); longer strokes are sped up. */\n maxStrokeMs?: number;\n}\n\nconst FRAME = 16;\n\n/** Where along a stroke each point is reached, 0..1: by its time `t` when the points have times, else by distance. */\nexport function strokeSchedule(points: readonly PenPoint[]): number[] {\n if (points.length < 2) return points.map(() => 1);\n const t0 = points[0]!.t;\n const t1 = points[points.length - 1]!.t;\n if (points.every((p) => Number.isFinite(p.t)) && (t1 as number) > (t0 as number)) {\n return points.map((p) => Math.min(1, Math.max(0, ((p.t as number) - (t0 as number)) / ((t1 as number) - (t0 as number)))));\n }\n const acc = [0];\n for (let i = 1; i < points.length; i++) acc.push(acc[i - 1]! + Math.hypot(points[i]!.x - points[i - 1]!.x, points[i]!.y - points[i - 1]!.y));\n const total = acc[acc.length - 1]!;\n return total > 0 ? acc.map((d) => d / total) : points.map(() => 1);\n}\n\n/** How long a stroke takes to paint at speed 1 (ms): its recorded duration, else ~1.5 px/ms, between 250 ms and `max`. */\nexport function strokeDuration(points: readonly PenPoint[], max = 1800): number {\n const t0 = points[0]?.t;\n const t1 = points[points.length - 1]?.t;\n let ms: number;\n if (points.every((p) => Number.isFinite(p.t)) && (t1 as number) > (t0 as number)) ms = (t1 as number) - (t0 as number);\n else {\n let length = 0;\n for (let i = 1; i < points.length; i++) length += Math.hypot(points[i]!.x - points[i - 1]!.x, points[i]!.y - points[i - 1]!.y);\n ms = length / 1.5;\n }\n return Math.min(max, Math.max(250, ms));\n}\n\n/** A zig-zag across `rect` (for a fill rubbed over the sheet), `rows` passes from the top. */\nexport function sweepPath(rect: RasterRect, rows = 4): Array<{ x: number; y: number }> {\n const inset = Math.min(rect.width, rect.height) * 0.08;\n const left = rect.x + inset;\n const right = rect.x + rect.width - inset;\n const out: Array<{ x: number; y: number }> = [];\n for (let r = 0; r < rows; r++) {\n const y = rect.y + inset + ((rect.height - 2 * inset) * (r + 0.5)) / rows;\n out.push(r % 2 ? { x: right, y } : { x: left, y }, r % 2 ? { x: left, y } : { x: right, y });\n }\n return out;\n}\n\nconst centre = (r: RasterRect) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });\n\n/**\n * Acts out agent commands on a paint canvas: the actor's cursor glides to the stroke's start and\n * paints it live along its points (paced by their times, or evenly), the stroke committing as\n * one undo step; fills are rubbed across the sheet first; replays start from the strokes they\n * copy and end on what they painted. Commands queue per actor. A command that cannot apply (an\n * unknown or locked layer, a stroke the user is still painting) fails with the controller's\n * sentence before anything is painted.\n */\nexport class PaintDirector {\n private readonly clock: PresenceClock;\n private readonly queue: PerformanceQueue;\n\n constructor(\n private readonly controller: PaintEditorController,\n readonly presence: PaintPresence,\n private readonly options: PaintDirectorOptions = {}\n ) {\n this.clock = options.clock ?? realPresenceClock;\n this.queue = new PerformanceQueue(presence, { clock: this.clock, linger: options.linger });\n }\n\n /** Acts out `commands` in order for `actor`. Resolves with one result per command. */\n perform(actor: PresenceActorInfo, commands: readonly PaintAgentCommand[]): Promise<PaintPerformanceResult[]> {\n return this.queue.run(actor, async () => {\n const results: PaintPerformanceResult[] = [];\n for (const command of commands) {\n try {\n results.push({ ok: true, command, result: await this.performOne(actor.id, command) });\n } catch (error) {\n results.push({ ok: false, command, message: error instanceof Error ? error.message : String(error) });\n }\n }\n return results;\n });\n }\n\n private ms(n: number): number {\n return Math.round(n / Math.max(0.1, this.options.speed ?? 1));\n }\n\n private where(id: string) {\n const a = this.presence.get(id);\n return { x: a?.x ?? 0, y: a?.y ?? 0 };\n }\n\n /** Glides to `to`; an actor that just appeared starts there instead of the corner. */\n private async glide(id: string, to: { x: number; y: number }, ms = 380) {\n const a = this.presence.get(id);\n if (!a || (a.x === 0 && a.y === 0)) {\n this.presence.set({ id, x: to.x, y: to.y });\n return;\n }\n await tweenPresenceCursor(this.presence, this.clock, id, this.where(id), to, this.ms(ms));\n }\n\n private async say(id: string, text: string | undefined, ms: number) {\n if (!text) return;\n this.presence.set({ id, message: text });\n await this.clock.wait(this.ms(ms));\n }\n\n private async performOne(id: string, command: PaintAgentCommand): Promise<unknown> {\n const c = this.controller;\n switch (command.kind) {\n case 'say':\n await this.say(id, command.text, 900);\n return undefined;\n case 'point':\n await this.say(id, command.note, 500);\n await this.glide(id, command.at);\n this.presence.set({ id, pressed: true });\n await this.clock.wait(this.ms(110));\n this.presence.set({ id, pressed: false });\n return undefined;\n case 'stroke':\n await this.say(id, command.note, 500);\n return this.stroke(id, command.stroke);\n case 'fill': {\n await this.say(id, command.note, 500);\n const { width, height } = c.getState().document.canvas;\n const path = sweepPath({ x: 0, y: 0, width, height });\n await this.glide(id, path[0]!);\n this.presence.set({ id, pressed: true });\n try {\n for (let i = 1; i < path.length; i++) await tweenPresenceCursor(this.presence, this.clock, id, path[i - 1]!, path[i]!, this.ms(i % 2 ? 260 : 90));\n } finally {\n this.presence.set({ id, pressed: false });\n }\n return c.fillLayer(command.fill);\n }\n case 'replay': {\n await this.say(id, command.note, 500);\n const source = c.getStrokes().filter((s) => command.replay.strokeIds?.includes(s.id));\n const first = source[0]?.points[0];\n if (first) await this.glide(id, first);\n const result = c.replayStrokes(command.replay);\n if (result.bounds) await this.glide(id, centre(result.bounds), 520);\n return result;\n }\n }\n }\n\n /** Paints a stroke live: begins it, pushes its points frame by frame as the cursor reaches them, then ends it (one undo step). */\n private async stroke(id: string, init: PaintStrokeInit & { points: readonly PenPoint[] }): Promise<PaintStrokeResult> {\n const c = this.controller;\n const points = c.checkPoints(init.points);\n const { points: _points, ...rest } = init;\n // Validate the layer and brush (and that nothing else is painting) before moving at all.\n const session = c.beginStroke({ snapToPerspective: false, start: points[0], ...rest });\n try {\n await this.glide(id, points[0]!);\n this.presence.set({ id, pressed: true });\n const at = strokeSchedule(points);\n const duration = this.ms(strokeDuration(points, this.options.maxStrokeMs));\n let next = 0;\n const frames = Math.max(1, Math.round(duration / FRAME));\n for (let f = 1; f <= frames; f++) {\n const t = f / frames;\n const from = next;\n while (next < points.length && at[next]! <= t + 1e-9) next++;\n if (next > from) {\n session.push(points.slice(from, next));\n const p = points[next - 1]!;\n this.presence.set({ id, x: p.x, y: p.y });\n }\n if (f < frames) await this.clock.wait(duration / frames);\n }\n if (next < points.length) session.push(points.slice(next));\n this.presence.set({ id, pressed: false });\n return session.end();\n } catch (error) {\n if (c.activeStroke === session) session.cancel();\n this.presence.set({ id, pressed: false });\n throw error;\n }\n }\n}\n","/**\n * Agent tools for stickers: stamps sprayed with a sticker brush (`paint_stroke`, list_brushes\n * category sticker) stay objects on their layer, which these tools list, move, turn, resize,\n * recolour, reorder and delete. Each change is one undo step and redraws the layer.\n */\n\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\nimport { STAMP_KINDS } from './stickers';\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;\nconst LAYER_ID = { type: 'string', description: 'Sticker layer id from list_layers (default the active layer).' } as const;\n\nexport function createPaintStickerTools(c: PaintEditorController): AgentTool[] {\n return [\n {\n name: 'list_stickers',\n title: 'List stickers',\n description: 'Lists a layer’s stickers bottom first: id, stamp, x, y (centre, document pixels), size (diameter px), rotation (degrees clockwise), color, opacity. Empty for a layer without stickers. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'List stickers', ...READ },\n handler: ({ layerId }) => ({ layerId: layerId ?? c.document.activeLayerId, stickers: c.listStickers(layerId) }),\n },\n {\n name: 'update_sticker',\n title: 'Change a sticker',\n description: 'Moves, turns, resizes, recolours, fades or restamps one sticker by id from list_stickers. One undo step. Returns the sticker.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n id: { type: 'string', description: 'Sticker id from list_stickers.' },\n stamp: { type: 'string', enum: [...STAMP_KINDS], description: 'Another stamp.' },\n x: { type: 'number', description: 'Centre x in document pixels.' },\n y: { type: 'number', description: 'Centre y in document pixels.' },\n size: { type: 'number', minimum: 2, maximum: 2048, description: 'Diameter in pixels.' },\n rotation: { type: 'number', minimum: -3600, maximum: 3600, description: 'Degrees clockwise.' },\n color: { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'Colour #rrggbb.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a sticker', ...WRITE },\n handler: ({ layerId, id, ...update }) => c.updateSticker(layerId, id, update),\n },\n {\n name: 'arrange_stickers',\n title: 'Bring stickers forward or back',\n description: 'Moves stickers to the top (front) or bottom (back) of their layer, keeping their order among themselves. One undo step. Returns the new order of ids.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n ids: { type: 'array', minItems: 1, items: { type: 'string', description: 'Sticker id.' }, description: 'Sticker ids from list_stickers.' },\n to: { type: 'string', enum: ['front', 'back'], description: 'front: on top of the others; back: under them.' },\n },\n required: ['ids', 'to'],\n additionalProperties: false,\n },\n annotations: { title: 'Bring stickers forward or back', ...WRITE },\n handler: ({ layerId, ids, to }) => ({ order: c.arrangeStickers(layerId, ids, to) }),\n },\n {\n name: 'remove_stickers',\n title: 'Delete stickers',\n description: 'Deletes stickers by id, or every sticker on the layer when ids is missing. One undo step. Returns the ids removed.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, ids: { type: 'array', items: { type: 'string', description: 'Sticker id.' }, description: 'Sticker ids (default all).' } }, additionalProperties: false },\n annotations: { title: 'Delete stickers', ...DESTRUCTIVE },\n handler: ({ layerId, ids }) => ({ removed: c.removeStickers(layerId, ids) }),\n },\n ];\n}\n","/**\n * Agent tools for the paint editor's painting guides (symmetry, perspective, tracing and reference\n * images) and colour (harmonies, swatches, the mixing pad). Guides, swatches and the pad are\n * editor state saved with the document (the pad is not saved), not undo steps.\n */\n\nimport { PAINT_SOURCE_JSON_SCHEMA } from '../paint/gradient-paint';\nimport type { AgentTool } from '../tools/registry.js';\nimport { bytesToBase64, encodePng } from '../raster/png';\nimport { HARMONY_KINDS } from './color';\nimport type { PaintEditorController } from './controller';\nimport { SYMMETRY_MODES, symmetryCopies, type ReferenceImage, type TracingImage } from './guides';\nimport type { PenPoint } from './engine';\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 SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\nconst EMPTY = { type: 'object', properties: {}, additionalProperties: false } as const;\nconst COLOR = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'Colour #rrggbb.' } as const;\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'x in document pixels.' }, y: { type: 'number', description: 'y in document pixels.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\nconst RECT = {\n type: 'object',\n description: 'Placement in document pixels.',\n properties: {\n x: { type: 'number', description: 'Left edge.' },\n y: { type: 'number', description: 'Top edge.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height.' },\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false,\n} as const;\nconst PNG = { type: 'string', minLength: 1, description: 'The image as a PNG, base64 or a data:image/png URL.' } as const;\n\nconst tracingInfo = (t: TracingImage | null) => (t ? { width: t.width, height: t.height, rect: t.rect, opacity: t.opacity, visible: t.visible, sampleColors: t.sampleColors } : null);\nconst refInfo = ({ src: _s, ...r }: ReferenceImage) => r;\n\nexport function createPaintGuideTools(c: PaintEditorController): AgentTool[] {\n const guides = () => c.getState().guides;\n const STENCIL_GEOMETRY = {\n mode: { type: 'string', enum: ['guide', 'mask'], description: 'guide: strokes that start within snap px of the edge follow it; mask: paint lands only inside (or on the left of a line going from → to), outside with invert. Default guide for ruler and arc, mask for line and shape.' },\n enabled: { type: 'boolean', description: 'In effect and shown (default true).' },\n from: { ...POINT, description: 'ruler / line: a point on the edge.' },\n to: { ...POINT, description: 'ruler / line: another point on the edge (the edge runs on past both).' },\n center: { ...POINT, description: 'arc: the centre.' },\n radius: { type: 'number', exclusiveMinimum: 0, description: 'arc: the radius in pixels.' },\n startAngle: { type: 'number', minimum: -720, maximum: 720, description: 'arc: where it starts, degrees clockwise from +x (default 0).' },\n endAngle: { type: 'number', minimum: -720, maximum: 720, description: 'arc: where it ends (default 360: the whole circle).' },\n shape: { type: 'string', enum: ['rect', 'ellipse', 'polygon'], description: 'shape: rect or ellipse (with rect) or polygon (with points).' },\n rect: { ...RECT, description: 'shape rect / ellipse: its bounds in document pixels.' },\n points: { type: 'array', minItems: 3, maxItems: 500, items: POINT, description: 'shape polygon: its corners in order.' },\n invert: { type: 'boolean', description: 'mask: paint outside the shape (or on the right of the line) instead (default false).' },\n snap: { type: 'number', minimum: 1, maximum: 400, description: 'guide: how near the edge a stroke must start to follow it, px (default 40).' },\n feather: { type: 'number', minimum: 0, maximum: 100, description: 'mask: soft edge width in pixels (default 1).' },\n } as const;\n return [\n {\n name: 'get_guides',\n title: 'Get the painting guides',\n description: 'Reads the painting guides: symmetry (mode, centre, rotation, segments, mirror, and how many copies each stroke paints), the perspective guide (vanishing points, snap), the tracing image (size, placement, opacity, visibility, colour sampling), the reference images and the stencils (id, kind, mode, geometry).',\n inputSchema: EMPTY,\n annotations: { title: 'Get the painting guides', ...READ },\n handler: () => {\n const g = guides();\n return { symmetry: { ...g.symmetry, copies: symmetryCopies(g.symmetry).length }, perspective: g.perspective, tracing: tracingInfo(g.tracing), references: g.references.map(refInfo), stencils: g.stencils };\n },\n },\n {\n name: 'set_symmetry',\n title: 'Set mirror or kaleidoscope painting',\n description:\n 'Turns symmetry on or off. Every stroke after it also paints its copies, as one stroke and one undo step: vertical, horizontal or diagonal mirror (one copy), dual (four-way), or kaleidoscope with n segments (rotated copies, and with mirror the reflections between them). Editor state, saved with the document. Returns the guide and the number of copies.',\n inputSchema: {\n type: 'object',\n properties: {\n mode: { type: 'string', enum: [...SYMMETRY_MODES], description: 'off, vertical (mirror left / right), horizontal (top / bottom), dual, diagonal or kaleidoscope.' },\n center: { ...POINT, description: 'Centre of the axes in document pixels (default the canvas centre).' },\n rotation: { type: 'number', minimum: -360, maximum: 360, description: 'Turn the axes, degrees clockwise.' },\n segments: { type: 'integer', minimum: 2, maximum: 32, description: 'Kaleidoscope segments (default 6).' },\n mirror: { type: 'boolean', description: 'Kaleidoscope: also reflect alternate segments (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set mirror or kaleidoscope painting', ...SET },\n handler: (a) => {\n const s = c.setSymmetry(a);\n return { ...s, copies: symmetryCopies(s).length };\n },\n },\n {\n name: 'set_perspective_guide',\n title: 'Set the perspective guide',\n description:\n 'Sets one to three vanishing points (document pixels, may be off the canvas). With snap on, pen strokes snap to the line through their start towards a vanishing point (one-point also keeps horizontals and verticals, two-point verticals); for paint_stroke pass snapToPerspective. Editor state, saved with the document. Returns the guide.',\n inputSchema: {\n type: 'object',\n properties: {\n enabled: { type: 'boolean', description: 'Show and use the guide.' },\n points: { type: 'array', minItems: 1, maxItems: 3, items: { ...POINT, description: 'A vanishing point.' }, description: 'The vanishing points.' },\n snap: { type: 'boolean', description: 'Snap strokes to the guide lines (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the perspective guide', ...SET },\n handler: (a) => c.setPerspective(a),\n },\n {\n name: 'set_tracing_image',\n title: 'Set a tracing image',\n description:\n 'Puts an image under the paint to trace over (view only: never exported), fitted to the canvas unless rect is given, at opacity (default 0.35). With sampleColors, strokes without a colour take the image colour where they start, so painting over it copies its colours. Replaces any tracing image. Returns its size and placement.',\n inputSchema: {\n type: 'object',\n properties: {\n png: PNG,\n rect: RECT,\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1 (default 0.35).' },\n sampleColors: { type: 'boolean', description: 'Strokes take their colour from the image (default false).' },\n },\n required: ['png'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a tracing image', ...WRITE },\n handler: (a) => tracingInfo(c.setTracingImage(a)),\n },\n {\n name: 'update_tracing_image',\n title: 'Change the tracing image',\n description: 'Changes the tracing image opacity, visibility, placement or colour sampling. Returns it.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1.' },\n visible: { type: 'boolean', description: 'Show or hide it.' },\n sampleColors: { type: 'boolean', description: 'Strokes take their colour from it.' },\n rect: RECT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Change the tracing image', ...SET },\n handler: (a) => tracingInfo(c.updateTracing(a)),\n },\n {\n name: 'clear_tracing_image',\n title: 'Remove the tracing image',\n description: 'Removes the tracing image. Returns null.',\n inputSchema: EMPTY,\n annotations: { title: 'Remove the tracing image', ...DESTRUCTIVE },\n handler: () => {\n c.clearTracingImage();\n return { tracing: null };\n },\n },\n {\n name: 'add_reference_image',\n title: 'Add a reference image',\n description: 'Adds a floating reference image (shown beside the canvas, never painted) to look at and pick colours from with pick_reference_color. Returns its id, size and placement.',\n inputSchema: {\n type: 'object',\n properties: {\n png: PNG,\n name: { type: 'string', minLength: 1, description: 'Display name.' },\n x: { type: 'number', description: 'Panel left in viewport pixels.' },\n y: { type: 'number', description: 'Panel top in viewport pixels.' },\n displayWidth: { type: 'number', minimum: 16, maximum: 4096, description: 'Panel width in screen pixels (default up to 240).' },\n },\n required: ['png'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a reference image', ...WRITE },\n handler: (a) => refInfo(c.addReferenceImage(a)),\n },\n {\n name: 'list_reference_images',\n title: 'List reference images',\n description: 'Lists the reference images: id, name, size in pixels, panel placement and visibility.',\n inputSchema: EMPTY,\n annotations: { title: 'List reference images', ...READ },\n handler: () => ({ references: guides().references.map(refInfo) }),\n },\n {\n name: 'update_reference_image',\n title: 'Change a reference image',\n description: 'Moves, resizes, renames, shows or hides a reference image panel. Returns it.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Reference id from list_reference_images.' },\n name: { type: 'string', minLength: 1, description: 'Display name.' },\n x: { type: 'number', description: 'Panel left in viewport pixels.' },\n y: { type: 'number', description: 'Panel top in viewport pixels.' },\n displayWidth: { type: 'number', minimum: 16, maximum: 4096, description: 'Panel width in screen pixels.' },\n visible: { type: 'boolean', description: 'Show or hide it.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a reference image', ...SET },\n handler: ({ id, ...u }) => refInfo(c.updateReferenceImage(id, u)),\n },\n {\n name: 'remove_reference_image',\n title: 'Remove a reference image',\n description: 'Removes a reference image. Returns the removed id.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Reference id from list_reference_images.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Remove a reference image', ...DESTRUCTIVE },\n handler: ({ id }) => {\n c.removeReferenceImage(id);\n return { removed: id };\n },\n },\n {\n name: 'pick_reference_color',\n title: 'Pick a colour from a reference',\n description: 'Reads the colour of a reference image at its own pixel (x, y). With setColor it becomes the paint colour. Returns #rrggbb.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Reference id from list_reference_images.' },\n x: { type: 'number', minimum: 0, description: 'x in the image’s pixels.' },\n y: { type: 'number', minimum: 0, description: 'y in the image’s pixels.' },\n setColor: { type: 'boolean', description: 'Also make it the paint colour (default false).' },\n },\n required: ['id', 'x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Pick a colour from a reference', ...SET },\n handler: ({ id, x, y, setColor }) => {\n const color = c.pickReferenceColor(id, x, y);\n if (setColor) c.setColor(color);\n return { color };\n },\n },\n {\n name: 'color_harmony',\n title: 'Colour harmonies',\n description:\n 'Returns the colours of a harmony around a colour (default the paint colour): complementary, split-complementary, analogous, triadic, tetradic, square or monochromatic, on the painter’s red-yellow-blue wheel (default: red’s complement is green) or the RGB wheel. Changes nothing. Returns the colours with their wheel angles, the base first.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: [...HARMONY_KINDS], description: 'The harmony.' },\n color: { ...COLOR, description: 'Base colour #rrggbb (default the paint colour).' },\n wheel: { type: 'string', enum: ['ryb', 'rgb'], description: 'ryb (painter’s wheel, default) or rgb.' },\n spread: { type: 'number', minimum: 5, maximum: 90, description: 'Analogous and split-complementary spread in degrees (default 30).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Colour harmonies', ...READ },\n handler: ({ kind, color, wheel, spread }) => ({ kind, wheel: wheel ?? 'ryb', colors: c.harmony(kind, { color, wheel, spread }) }),\n },\n {\n name: 'list_swatches',\n title: 'List swatches',\n description: 'Lists the swatches (index, colour, name, and paint for a gradient swatch).',\n inputSchema: EMPTY,\n annotations: { title: 'List swatches', ...READ },\n handler: () => ({ swatches: c.getState().swatches.map((s, index) => ({ index, ...s })) }),\n },\n {\n name: 'add_swatch',\n title: 'Add a swatch',\n description: 'Adds a swatch (saved with the document): a colour, or with paint a gradient (set_paint picks it up again from list_swatches). Give color or paint. Returns the swatches; a gradient swatch has paint and its first colour as color.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { ...COLOR, description: 'A colour #rrggbb.' },\n paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: 'Gradient paint (or a colour) for the swatch, instead of color.' },\n name: { type: 'string', minLength: 1, description: 'Optional name.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a swatch', ...WRITE },\n handler: ({ color, paint, name }) => {\n if ((color === undefined) === (paint === undefined)) throw new Error('Give color (#rrggbb) or paint (a colour or gradient), not both and not neither.');\n return { swatches: c.addSwatch(paint ?? color, name).map((s, index) => ({ index, ...s })) };\n },\n },\n {\n name: 'remove_swatch',\n title: 'Remove a swatch',\n description: 'Removes a swatch by index (from list_swatches). Returns the swatches.',\n inputSchema: { type: 'object', properties: { index: { type: 'integer', minimum: 0, description: '0-based index.' } }, required: ['index'], additionalProperties: false },\n annotations: { title: 'Remove a swatch', ...DESTRUCTIVE },\n handler: ({ index }) => ({ swatches: c.removeSwatch(index).map((s, i) => ({ index: i, ...s })) }),\n },\n {\n name: 'mixing_pad_stroke',\n title: 'Paint on the mixing pad',\n description:\n 'Paints on the mixing pad (a 360 × 220 scratch canvas, not the painting): tool paint lays down a colour with a pigment Mixer Brush; tool knife smears what is there like a palette knife, mixing it as paint. Points are pad pixels. Not an undo step. Returns the changed bounds; then pick_mixing_pad_color. For a measured mix in one call use mix_on_pad.',\n inputSchema: {\n type: 'object',\n properties: {\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 2000,\n description: 'The path in pad pixels (0..360, 0..220).',\n items: {\n type: 'object',\n properties: { x: { type: 'number', description: 'Pad x.' }, y: { type: 'number', description: 'Pad y.' }, pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pressure (default 1).' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n tool: { type: 'string', enum: ['paint', 'knife'], description: 'paint (default) or knife.' },\n color: { ...COLOR, description: 'Paint colour (default the paint colour; ignored by the knife).' },\n size: { type: 'number', minimum: 1, maximum: 200, description: 'Tool size in pad pixels (default 28 paint, 34 knife).' },\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint on the mixing pad', ...WRITE },\n handler: ({ points, tool, color, size }) => c.mixingPadStroke(points as PenPoint[], { tool, color, size }),\n },\n {\n name: 'pick_mixing_pad_color',\n title: 'Pick from the mixing pad',\n description: 'Reads the mixed colour on the pad at (x, y), averaged over radius pixels (default 3); with setColor it becomes the paint colour. Returns #rrggbb, or null where the pad is bare.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, maximum: 360, description: 'Pad x.' },\n y: { type: 'number', minimum: 0, maximum: 220, description: 'Pad y.' },\n radius: { type: 'integer', minimum: 0, maximum: 20, description: 'Averaging radius in pixels (default 3).' },\n setColor: { type: 'boolean', description: 'Also make it the paint colour (default false).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Pick from the mixing pad', ...SET },\n handler: ({ x, y, radius, setColor }) => ({ color: c.mixingPadPick(x, y, { radius, setColor }) }),\n },\n {\n name: 'clear_mixing_pad',\n title: 'Clear the mixing pad',\n description: 'Wipes the mixing pad clean.',\n inputSchema: EMPTY,\n annotations: { title: 'Clear the mixing pad', ...DESTRUCTIVE },\n handler: () => {\n c.clearMixingPad();\n return { cleared: true };\n },\n },\n {\n name: 'mix_on_pad',\n title: 'Mix paints on the pad',\n description:\n 'Mixes paints on the mixing pad as a painter would: blobs whose areas follow their parts, worked with the palette knife, and a fully mixed core of the Kubelka-Munk (pigment) mix, so blue and yellow make green and white tints. One call per mix. Not an undo step (the pad is scratch space). Returns the mixed colour #rrggbb and the pile bounds in pad pixels; with setColor it becomes the paint colour. pick_mixing_pad_color reads the streaks around it.',\n inputSchema: {\n type: 'object',\n properties: {\n paints: {\n type: 'array',\n minItems: 1,\n maxItems: 8,\n description: 'The paints, 1 to 8.',\n items: {\n type: 'object',\n properties: {\n color: { ...COLOR, description: 'Paint colour #rrggbb.' },\n parts: { type: 'number', minimum: 0, description: 'Parts of this paint, any scale (1 and 3 for a 1:3 mix); at least one above 0.' },\n },\n required: ['color', 'parts'],\n additionalProperties: false,\n },\n },\n x: { type: 'number', minimum: 0, maximum: 360, description: 'Centre of the pile, pad x (default 180).' },\n y: { type: 'number', minimum: 0, maximum: 220, description: 'Centre of the pile, pad y (default 110).' },\n radius: { type: 'number', minimum: 12, maximum: 100, description: 'Radius of the pile in pad pixels (default 40); it must fit on the pad.' },\n passes: { type: 'integer', minimum: 1, maximum: 20, description: 'Palette-knife passes (default 3; more gives finer streaks around the core).' },\n clear: { type: 'boolean', description: \"Wipe the pile's area first (default true); false mixes into the paint already there.\" },\n setColor: { type: 'boolean', description: 'Also make the mix the paint colour (default false).' },\n },\n required: ['paints'],\n additionalProperties: false,\n },\n annotations: { title: 'Mix paints on the pad', ...WRITE },\n handler: ({ paints, x, y, radius, passes, clear, setColor }) => c.mixOnPad(paints, { x, y, radius, passes, clear, setColor }),\n },\n {\n name: 'save_mixing_pad',\n title: 'Keep the mixing pad',\n description: 'Keeps the mixing pad as it is now under a name, saved with the document, to come back to with load_mixing_pad. Refused while the pad is empty. Returns the kept pad id and name.',\n inputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 80, description: 'Name (default \"Pad n\").' } }, additionalProperties: false },\n annotations: { title: 'Keep the mixing pad', ...WRITE },\n handler: ({ name }) => c.saveMixingPad(name),\n },\n {\n name: 'list_mixing_pads',\n title: 'List kept mixing pads',\n description: 'Lists the mixing pads kept with save_mixing_pad (id, name). Changes nothing.',\n inputSchema: EMPTY,\n annotations: { title: 'List kept mixing pads', ...READ },\n handler: () => ({ pads: c.listMixingPads() }),\n },\n {\n name: 'load_mixing_pad',\n title: 'Load a kept mixing pad',\n description: 'Puts a kept pad (id from list_mixing_pads) back on the mixing pad, replacing what is on it. Returns the pad id and name.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Kept pad id from list_mixing_pads.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Load a kept mixing pad', ...DESTRUCTIVE },\n handler: ({ id }) => c.loadMixingPad(id),\n },\n {\n name: 'remove_mixing_pad',\n title: 'Remove a kept mixing pad',\n description: 'Forgets a kept pad (id from list_mixing_pads); the pad on screen is untouched. Returns the removed id and name.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Kept pad id from list_mixing_pads.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Remove a kept mixing pad', ...DESTRUCTIVE },\n handler: ({ id }) => c.removeMixingPad(id),\n },\n {\n name: 'render_mixing_pad',\n title: 'Render the mixing pad',\n description: 'Renders the mixing pad as a PNG (360 × 220, over white). Changes nothing.',\n inputSchema: EMPTY,\n annotations: { title: 'Render the mixing pad', ...READ },\n handler: () => {\n const pad = c.mixingPad;\n return {\n content: [\n { type: 'image', data: bytesToBase64(encodePng(pad.width, pad.height, pad.pixels())), mimeType: 'image/png' },\n { type: 'text', text: `Mixing pad ${pad.width}×${pad.height}` },\n ],\n };\n },\n },\n {\n name: 'add_stencil',\n title: 'Lay a stencil',\n description:\n 'Lays a stencil on the canvas: a ruler (straight edge to draw along) or an arc (circle or part of one) that pointer and agent strokes starting near it follow exactly, or a line (straight masking edge) or a shape (rect, ellipse or polygon template) that paint lands only inside of (outside with invert). Editor state saved with the document, not an undo step; replays keep the mask they were painted through. Returns the stencil with its id.',\n inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['ruler', 'arc', 'line', 'shape'], description: 'ruler, arc, line or shape.' }, ...STENCIL_GEOMETRY }, required: ['kind'], additionalProperties: false },\n annotations: { title: 'Lay a stencil', ...WRITE },\n handler: (a) => c.addStencil(a as Parameters<PaintEditorController[\"addStencil\"]>[0]),\n },\n {\n name: 'update_stencil',\n title: 'Change a stencil',\n description: 'Moves, reshapes, switches (guide or mask, invert) or hides (enabled false) a stencil by id from get_guides. Returns the stencil.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Stencil id from get_guides.' }, ...STENCIL_GEOMETRY }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Change a stencil', ...SET },\n handler: ({ id, ...update }) => c.updateStencil(id, update),\n },\n {\n name: 'remove_stencils',\n title: 'Take stencils away',\n description: 'Removes stencils by id, or every stencil when ids is missing. Returns the ids removed.',\n inputSchema: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string', description: 'Stencil id.' }, description: 'Stencil ids from get_guides (default all).' } }, additionalProperties: false },\n annotations: { title: 'Take stencils away', ...DESTRUCTIVE },\n handler: ({ ids }) => ({ removed: c.removeStencils(ids) }),\n },\n ];\n}\n","/**\n * Agent tools for wet media (watercolour and gouache on a simulated wet layer). Wet strokes are\n * painted with `paint_stroke` and a wet brush (`list_brushes` category `watercolor` or\n * `wet-tool`); these tools read and steer the wet paint. Every result says whether paint is still\n * wet. Settings (wet parameters, flow maps, tilt) are saved with the document but are not undo\n * steps; drying and wetting the paper are.\n */\n\nimport { WET_FLOW_KINDS } from '../paint/wet/flow';\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\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 SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst EMPTY = { type: 'object', properties: {}, additionalProperties: false } as const;\nconst LAYER_ID = { type: 'string', description: 'Pixel layer id from list_layers (default the active layer).' } as const;\nconst RECT = {\n type: 'object',\n description: 'A rectangle in document pixels.',\n properties: {\n x: { type: 'number', description: 'Left edge, document pixels.' },\n y: { type: 'number', description: 'Top edge, document pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in pixels.' },\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false,\n} as const;\n\nconst UNIT = (d: string) => ({ type: 'number', minimum: 0, maximum: 1, description: d }) as const;\n\nexport const WET_PARAM_SCHEMA = {\n viscosity: UNIT('How thick the water is: 0 runny like ink .. 1 very thick (default 0.3, watercolour).'),\n evaporation: UNIT('How fast it dries: 0 never .. 1 very fast (default 0.45).'),\n edgeDarkening: UNIT('Darker rims where a wash dries (default 0.5).'),\n pigmentWeight: UNIT('Heavy pigment travels less and settles sooner (default 0.3).'),\n granulation: UNIT('Pigment collecting in the paper’s valleys, a grainy look (default 0.4; needs a rough paper).'),\n pickup: UNIT('How readily water lifts settled pigment again (default 0.2).'),\n fixture: UNIT('How fast pigment settles into the paper while wet, i.e. staining (default 0.2).'),\n bloom: UNIT('Blooms: water running into a drying wash pushes its pigment into cauliflower edges (default 0.4).'),\n bleed: UNIT('Colour bleed between touching wet areas (default 0.3).'),\n} as const;\n\nexport function createPaintWetTools(c: PaintEditorController): AgentTool[] {\n const stillWet = () => c.wetState().wet;\n return [\n {\n name: 'get_wet_paint',\n title: 'Get the wet paint',\n description:\n 'Reads the wet media: whether any paint is still wet, the simulation clock (steps), whether time is paused, its speed (steps per second), the canvas tilt, the kernel running it (typescript, wasm, wasm-threads with the thread count, and a message when it runs slower than it could, e.g. without cross-origin isolation), and each wet layer: wet cells, where the water is (wetBounds), live tiles, its settings and flow map. Also engine: Engine B, the WebGPU accelerator (mode auto/off, state, reason, adapter, minTiles, activeTiles). Call it after wet strokes to see what is still wet.',\n inputSchema: EMPTY,\n annotations: { title: 'Get the wet paint', ...READ },\n handler: () => c.wetState(),\n },\n {\n name: 'advance_wet_paint',\n title: 'Let the wet paint flow',\n description:\n 'Lets time pass for wet paint: the water spreads, runs downhill on a tilted canvas, granulates, blooms and evaporates, and pigment settles. Give steps (simulation steps; at the default 120 per second, 120 steps is one second) or seconds (default 1). Not an undo step, as time passing never is. Returns the steps run, the clock, stillWet and each wet layer’s state. Paint dries by itself after a few thousand steps; dry_paint dries it at once.',\n inputSchema: {\n type: 'object',\n properties: {\n steps: { type: 'integer', minimum: 0, maximum: 100000, description: 'Simulation steps to run (default: seconds × speed).' },\n seconds: { type: 'number', minimum: 0, maximum: 600, description: 'Seconds of simulated time at the current speed (default 1; ignored when steps is given).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Let the wet paint flow', ...WRITE },\n handler: async ({ steps, seconds }) => {\n await c.wetIdle();\n return c.advanceWet({ steps, seconds });\n },\n },\n {\n name: 'dry_paint',\n title: 'Dry the wet paint',\n description:\n 'Dries wet paint at once: everything settles where it is and the water goes, as a hair dryer at full blast would (the same deterministic bake that saving uses). On one layer or every wet layer, optionally only inside rect. Dried washes stay put: later washes glaze over them instead of mixing in. One undo step. Returns dried (each layer id with the rectangle that dried) and stillWet.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Only this layer (default every wet layer).' }, rect: RECT }, additionalProperties: false },\n annotations: { title: 'Dry the wet paint', ...WRITE },\n handler: async ({ layerId, rect }) => {\n await c.wetIdle();\n return c.dryPaint({ layerId, rect });\n },\n },\n {\n name: 'wet_paper',\n title: 'Pre-wet the paper',\n description:\n 'Wets the paper with clear water over rect (default the whole canvas) on a layer, for wet-in-wet: paint laid on damp paper spreads softly with feathered edges instead of stopping at a hard edge. One undo step. Returns the layer id, the rectangle and wet: true. The paper dries again over time (advance_wet_paint) or at once (dry_paint).',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, rect: RECT, amount: { type: 'number', exclusiveMinimum: 0, maximum: 1, description: 'How wet, 0..1 (default 1: soaked).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Pre-wet the paper', ...WRITE },\n handler: async ({ layerId, rect, amount }) => {\n await c.wetIdle();\n return c.wetPaper({ layerId, rect, amount });\n },\n },\n {\n name: 'set_wet_settings',\n title: 'Set a layer’s wet settings',\n description:\n 'Changes how wet paint behaves on a layer: viscosity, evaporation, edge darkening, pigment weight, granulation, pickup (re-wetting), fixture (staining), blooms and colour bleed, each 0..1. Missing fields keep their values. Applies to the paint already wet too. Saved with the document, not an undo step. Returns the settings and stillWet.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, ...WET_PARAM_SCHEMA }, additionalProperties: false },\n annotations: { title: 'Set a layer’s wet settings', ...SET },\n handler: async ({ layerId, ...params }) => {\n await c.wetIdle();\n return { layerId: layerId ?? c.document.activeLayerId, params: c.setWetParams(layerId, params), stillWet: stillWet() };\n },\n },\n {\n name: 'set_flow_map',\n title: 'Set a flow map',\n description:\n 'Pushes a layer’s water in a pattern while it is wet: linear (one direction, angle), swirl (around center, clockwise or not), radial (out from center) or turbulent (eddies of scale pixels, seeded), at strength 0..1; none turns it off. Saved with the document, not an undo step. Returns the flow map and stillWet.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: [...WET_FLOW_KINDS], description: 'none, linear, swirl, radial or turbulent.' },\n strength: UNIT('How hard it pushes, 0..1 (1 is about a 25° tilt; default 0.5).'),\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'linear: direction in degrees clockwise from canvas down (default 0).' },\n center: {\n type: 'object',\n description: 'swirl / radial: centre in document pixels (default the canvas centre).',\n properties: { x: { type: 'number', description: 'Document x.' }, y: { type: 'number', description: 'Document y.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n clockwise: { type: 'boolean', description: 'swirl: turn clockwise (default true).' },\n scale: { type: 'number', minimum: 8, maximum: 1024, description: 'turbulent: eddy size in pixels (default 120).' },\n seed: { type: 'integer', minimum: 0, description: 'turbulent: pattern seed (default 1).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a flow map', ...SET },\n handler: async ({ layerId, ...flow }) => {\n await c.wetIdle();\n return { layerId: layerId ?? c.document.activeLayerId, flow: c.setFlowMap(layerId, flow), stillWet: stillWet() };\n },\n },\n {\n name: 'set_canvas_tilt',\n title: 'Tilt the canvas',\n description:\n 'Tilts the canvas so wet paint runs downhill: angle 0 (flat) .. 90 (upright) degrees, direction the way down in degrees clockwise from the canvas bottom (0 runs towards the bottom edge, 90 towards the left). Steep tilts with very wet paint make drips. Saved with the document, not an undo step. Returns the tilt and stillWet.',\n inputSchema: {\n type: 'object',\n properties: {\n angle: { type: 'number', minimum: 0, maximum: 90, description: 'Degrees from flat (default unchanged).' },\n direction: { type: 'number', minimum: -360, maximum: 360, description: 'Downhill direction, degrees clockwise from canvas down (default unchanged).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Tilt the canvas', ...SET },\n handler: async ({ angle, direction }) => {\n await c.wetIdle();\n return { tilt: c.setCanvasTilt({ angle, direction }), stillWet: stillWet() };\n },\n },\n {\n name: 'pause_wet_paint',\n title: 'Pause or resume wet paint',\n description: 'Stops time for wet paint (it stays wet and still) or lets it run again, and optionally sets the speed in steps per second. Editor state, not an undo step. Returns paused, speed and stillWet.',\n inputSchema: {\n type: 'object',\n properties: {\n paused: { type: 'boolean', description: 'true stops time, false resumes it.' },\n speed: { type: 'number', minimum: 1, maximum: 2000, description: 'Steps per second while running (default unchanged; 120 at start).' },\n },\n required: ['paused'],\n additionalProperties: false,\n },\n annotations: { title: 'Pause or resume wet paint', ...SET },\n handler: ({ paused, speed }) => {\n c.setWetPaused(paused);\n if (speed !== undefined) c.setWetSpeed(speed);\n const s = c.getState().wet;\n return { paused: s.paused, speed: s.speed, stillWet: s.wet };\n },\n },\n {\n name: 'set_wet_engine',\n title: 'Choose the wet-media engine',\n description:\n 'Lets Engine B, the WebGPU accelerator, run for large wet areas (gpu \"auto\", the default) or forces the CPU (gpu \"off\"), and optionally sets minTiles, the wet area at which the GPU takes over. The GPU only shades what is shown: the CPU engine stays the authority, so the painting, drying, saving, export and replay come out the same either way; force the CPU when the display looks wrong or the GPU is busy with other work. Editor setting, not an undo step, not saved with the painting. Returns the engine: mode, state (off, not-started, starting, unavailable, standby, active, failed), reason, adapter, maxTiles, minTiles and activeTiles.',\n inputSchema: {\n type: 'object',\n properties: {\n gpu: { type: 'string', enum: ['auto', 'off'], description: '\"auto\": use the GPU when there is one and the wet area is large; \"off\": the CPU only.' },\n minTiles: { type: 'integer', minimum: 1, maximum: 1024, description: 'Live 256 px tiles (all wet layers) at which the GPU takes over (default unchanged; 16 at start, a fully wet 1024² canvas).' },\n },\n required: ['gpu'],\n additionalProperties: false,\n },\n annotations: { title: 'Choose the wet-media engine', ...SET },\n handler: async ({ gpu, minTiles }) => ({ engine: await c.setWetEngine({ gpu, minTiles }) }),\n },\n {\n name: 'set_water_overlay',\n title: 'Show the water',\n description: 'Shows or hides the water overlay on the canvas (blue where the surface is wet, fainter where the paper is damp). View only, never in exports. Returns visible and stillWet.',\n inputSchema: { type: 'object', properties: { visible: { type: 'boolean', description: 'Show (true) or hide (false) the overlay.' } }, required: ['visible'], additionalProperties: false },\n annotations: { title: 'Show the water', ...SET },\n handler: ({ visible }) => ({ visible: c.setWaterOverlay(visible), stillWet: stillWet() }),\n },\n ];\n}\n","/**\n * Agent tools for thick paint and light: impasto strokes are painted with `paint_stroke` and an\n * impasto brush; these tools move the light, read and flatten a layer's relief, and switch instant\n * drying of wet strokes.\n */\n\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst LAYER_ID = { type: 'string', description: 'Pixel layer id from list_layers (default the active layer).' } as const;\n\nexport function createPaintThickTools(c: PaintEditorController): AgentTool[] {\n return [\n {\n name: 'set_lighting',\n title: 'Move the light',\n description:\n 'Sets where the light comes from for thick paint (impasto relief) and metallic paper: azimuth (degrees clockwise from the top of the canvas; 315 is top left), elevation (5 grazing, deep shadows .. 90 straight on, no relief) and relief (0 flat .. 1 strong). Nothing is repainted: the relief is lit at render time. Saved with the document, not an undo step. Returns the lighting.',\n inputSchema: {\n type: 'object',\n properties: {\n azimuth: { type: 'number', minimum: -360, maximum: 360, description: 'Light direction in degrees clockwise from the top of the canvas (default 315).' },\n elevation: { type: 'number', minimum: 5, maximum: 90, description: 'Light height above the canvas in degrees (default 40).' },\n relief: { type: 'number', minimum: 0, maximum: 1, description: 'How strongly the relief shows (default 0.7).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Move the light', ...SET },\n handler: (a) => c.setLighting(a),\n },\n {\n name: 'get_relief',\n title: 'Read a layer’s relief',\n description: 'Reads a layer’s thick paint: where the relief is (bounds, layer pixels), its highest point (0..255) and the lighting. relief is null when the layer is flat. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'Read a layer’s relief', ...READ },\n handler: ({ layerId }) => ({ relief: c.reliefOf(layerId), lighting: c.getLighting() }),\n },\n {\n name: 'flatten_relief',\n title: 'Flatten thick paint',\n description: 'Removes a layer’s relief and material (gloss, metallic): the paint keeps its colour but lies flat. One undo step. Returns the layer id and whether anything was flattened.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'Flatten thick paint', ...WRITE },\n handler: ({ layerId }) => c.flattenRelief(layerId),\n },\n {\n name: 'set_instant_dry',\n title: 'Dry wet strokes at once',\n description: 'Switches instant drying: on, every wet stroke (watercolour, gouache, oil) dries as soon as it ends, in the same undo step, so strokes stay exactly where they are laid; off (default), wet paint keeps moving until it dries. Editor state. Returns instantDry.',\n inputSchema: { type: 'object', properties: { on: { type: 'boolean', description: 'true dries every wet stroke as it ends.' } }, required: ['on'], additionalProperties: false },\n annotations: { title: 'Dry wet strokes at once', ...SET },\n handler: ({ on }) => ({ instantDry: c.setInstantDry(on) }),\n },\n ];\n}\n","/**\n * Agent tools for replaying a painting from its data: a time-lapse (frames, a video project of the\n * frames and the ffmpeg command that renders it) and a high-resolution export repainted from the\n * recorded strokes.\n */\n\nimport { bytesToBase64, decodePng, encodePng } from '../raster/png';\nimport type { AgentTool } from '../tools/registry.js';\nimport type { PaintEditorController } from './controller';\nimport { contactSheet } from './replay';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nexport function createPaintReplayTools(c: PaintEditorController): AgentTool[] {\n return [\n {\n name: 'export_time_lapse',\n title: 'Export a time-lapse',\n description:\n 'Repaints the painting from its recorded strokes and events (fills, clears, wet-media events at their clock) in a fresh editor and returns a time-lapse: frame count, fps, size, duration, a video project of the frames as stills (assets frame-0001.png…), and the ffmpeg command that renders it to timelapse.mp4 from the frame files (the video editor export path). Shows a contact sheet of a few frames. With includeFrames the frames come back as base64 PNGs too. Changes nothing. Layers with pixels that were not painted with recorded strokes (imported images, merges) start empty in the replay.',\n inputSchema: {\n type: 'object',\n properties: {\n frames: { type: 'integer', minimum: 2, maximum: 600, description: 'About how many frames (default 120); one after every few strokes.' },\n fps: { type: 'number', minimum: 1, maximum: 60, description: 'Frames per second of the video (default 12).' },\n maxSide: { type: 'integer', minimum: 32, maximum: 4096, description: 'Longest side of each frame in pixels (default 720).' },\n wetStepsPerFrame: { type: 'integer', minimum: 0, maximum: 10000, description: 'While wet paint flows between strokes, a frame every this many simulation steps (default 60; 0 none).' },\n holdMs: { type: 'number', minimum: 0, maximum: 10000, description: 'Hold the finished painting this long at the end, ms (default 1500).' },\n includeFrames: { type: 'boolean', description: 'Also return every frame as base64 PNG (large; default false).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Export a time-lapse', ...READ },\n handler: async (a) => {\n await c.wetIdle();\n const tl = await c.timeLapse({ frames: a.frames, fps: a.fps, maxSide: a.maxSide, wetStepsPerFrame: a.wetStepsPerFrame, holdMs: a.holdMs });\n const pick = [0, Math.floor(tl.frames.length / 3), Math.floor((2 * tl.frames.length) / 3), tl.frames.length - 1].filter((v, i, arr) => arr.indexOf(v) === i);\n const sheet = contactSheet(pick.map((i) => decodePng(tl.frames[i])), pick.length, 160);\n const info = {\n frames: tl.frames.length,\n fps: tl.fps,\n width: tl.width,\n height: tl.height,\n durationMs: tl.durationMs,\n ffmpeg: tl.ffmpeg.command,\n video: tl.video,\n ...(a.includeFrames ? { framesPng: tl.frames.map((f) => bytesToBase64(f)) } : {}),\n };\n return { content: [{ type: 'image', data: bytesToBase64(encodePng(sheet.width, sheet.height, sheet.data)), mimeType: 'image/png' }, { type: 'text', text: JSON.stringify(info) }] };\n },\n },\n {\n name: 'export_high_res',\n title: 'Export at a higher resolution',\n description:\n 'Repaints the painting from its recorded strokes at scale (2..8) times the size, with brush sizes, paper grain, fills and wet events scaled, so the export has real detail instead of upscaled pixels; wet paint is dried. Returns a PNG as base64 with its size. Changes nothing. Layers with pixels that were not painted with recorded strokes come out empty; the canvas may be at most 16384 px per side.',\n inputSchema: {\n type: 'object',\n properties: {\n scale: { type: 'number', minimum: 1, maximum: 8, description: 'How many times larger (e.g. 2 doubles width and height).' },\n paper: { type: 'boolean', description: 'Include the paper (default true).' },\n },\n required: ['scale'],\n additionalProperties: false,\n },\n annotations: { title: 'Export at a higher resolution', ...READ },\n handler: async ({ scale, paper }) => {\n await c.wetIdle();\n const img = await c.renderHighRes({ scale, paper });\n const bytes = encodePng(img.width, img.height, img.data);\n return { mimeType: 'image/png', width: img.width, height: img.height, bytes: bytes.length, data: bytesToBase64(bytes) };\n },\n },\n ];\n}\n","/**\n * Agent tools for the paint editor (`createPaintEditorToolProvider`). Strict schemas, every\n * argument described, all four annotations. Each write is one undo step (or editor state, said so)\n * and returns what it changed; ids come from the read tools (`list_layers`, `list_brushes`,\n * `list_papers`, `list_strokes`).\n */\n\nimport type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { MIXING_MODELS, type MixerLoad } from '../paint/mixer';\nimport { PAINT_GRADIENT_JSON_SCHEMA, PAINT_SOURCE_JSON_SCHEMA } from '../paint/gradient-paint';\nimport { BLEND_MODES, standardBlendMode, type BlendMode } from '../raster/blend';\nimport { bytesToBase64, encodePng } from '../raster/png';\nimport { PARTICLE_KINDS, PARTICLE_RENDERS } from './procedural';\nimport { STAMP_KINDS } from './stickers';\nimport { createPaintStickerTools } from './sticker-tools';\nimport { BRUSH_ROTATIONS, IMPASTO_TOOLS, MAX_BRUSH_SIZE, MULTI_COLOR_MODES, PAINT_BRUSH_CATEGORIES, PAINT_BRUSH_ENGINES, WET_MEDIA, WET_TOOLS, brushToJSON, isWetCapable, type PaintBrush } from './brushes';\nimport { MAX_CANVAS_SIDE } from './document';\nimport type { PaintEditorController } from './controller';\nimport type { PenPoint } from './engine';\nimport { GRAIN_PATTERNS, PAPER_FAMILIES, PAPER_LIBRARY } from './paper';\nimport { MAX_ZOOM, MIN_ZOOM } from './view';\nimport { createPaintGuideTools } from './guide-tools';\nimport { createPaintWetTools } from './wet-tools';\nimport { createPaintThickTools } from './thick-tools';\nimport { createPaintReplayTools } from './replay-tools';\nimport type { PaintAgentCommand, PaintDirector } from './presence';\nimport type { PresenceActorInfo } from '../animation/presence';\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 SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst EMPTY = { type: 'object', properties: {}, additionalProperties: false } as const;\nconst COLOR = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'Colour #rrggbb, e.g. \"#2a4b8d\".' } as const;\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers (default the active layer).' } as const;\n\nexport interface PaintEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `paint-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n /**\n * Acts out strokes, fills and replays with an agent cursor on the canvas (see `PaintDirector`):\n * strokes paint live along their points. Each call is still one undo step. Without it, they apply at once.\n */\n director?: PaintDirector;\n /** The agent's cursor (default `{ id: 'agent', name: 'Agent' }`). */\n actor?: PresenceActorInfo;\n}\n\nconst TIP = {\n type: 'object',\n description: 'The tip (the shared brush engine). Missing fields keep their current values.',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in document pixels.' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness, 0 (soft) .. 1 (hard).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (0.05 is smooth, 1 is dotted).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees, counter-clockwise; with rotation stroke or barrel it is added to the stroke direction or pen rotation.' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness: 1 round, lower is a flat ellipse (a pastel or chisel tip).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity ceiling: overlapping dabs never exceed it.' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (low flow builds up softly, like an airbrush).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Position smoothing of the input, 0..0.95.' },\n pressureSize: { type: 'boolean', description: 'Pressure scales the size (down to dynamics.minSize).' },\n pressureOpacity: { type: 'boolean', description: 'Pressure scales the opacity.' },\n blend: {\n type: 'string',\n enum: BLEND_MODES.map((m) => m.id),\n description: 'How paint combines with the layer. pigment mixes it in like real paint (blue into yellow gives green); multiply darkens like marker ink; watercolor glazes like a transparent wash; tint, highlight and shadow recolour, light and shade (see list_blend_modes).',\n },\n },\n additionalProperties: false,\n} as const;\n\nconst DYNAMICS = {\n type: 'object',\n description: 'Pen dynamics and jitter. Missing fields keep their current values.',\n properties: {\n minSize: { type: 'number', minimum: 0, maximum: 1, description: 'Size at zero pressure as a fraction of the full size (when pressureSize).' },\n pressureFlow: { type: 'number', minimum: 0, maximum: 1, description: 'How much pressure scales flow.' },\n tiltSize: { type: 'number', minimum: 0, maximum: 1, description: 'Laying the pen down widens the tip (up to 2.5×).' },\n tiltRoundness: { type: 'number', minimum: 0, maximum: 1, description: 'Laying the pen down flattens the tip along the tilt (side-of-the-pencil shading).' },\n tiltOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Laying the pen down lightens the mark.' },\n velocitySize: { type: 'number', minimum: -1, maximum: 1, description: 'Positive grows the tip on fast strokes, negative thins it (needs point times t).' },\n velocityOpacity: { type: 'number', minimum: -1, maximum: 1, description: 'Positive darkens fast marks, negative lightens them (needs point times t).' },\n sizeJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random size change per dab.' },\n angleJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random angle per dab, as a fraction of ±180°.' },\n opacityJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random opacity drop per dab.' },\n scatter: { type: 'number', minimum: 0, maximum: 4, description: 'Random offset across the stroke, in diameters.' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXER = {\n type: 'object',\n description: 'Mixer engine settings (the Mixer Brush). Missing fields keep their current values.',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: 'Pick-up ratio: how much canvas colour each dab picks up, 0 (dry) .. 1.' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'How much paint the reservoir holds; below 1 it drains per dab, so long strokes run dry.' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"Mix ratio: how far each dab's paint moves from the reservoir towards the picked-up colour.\" },\n loadEachStroke: { type: 'boolean', description: 'Refill the reservoir with the colour before each stroke.' },\n cleanEachStroke: { type: 'boolean', description: 'Empty the pickup well after each stroke.' },\n mixing: { type: 'string', enum: [...MIXING_MODELS], description: 'rgb: RGB averages. pigment: mixes like paint (Kubelka-Munk).' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXING = { type: 'string', enum: [...MIXING_MODELS], description: 'pigment: mixes like paint (Kubelka-Munk, blue and yellow make green); rgb: straight RGB averages.' } as const;\n\nconst BEHAVIOR = {\n type: 'object',\n description:\n 'How the brush handles paint on the canvas (dab and mixer engines; on a mixer brush pickup, mix, dirty and mixing are its mixer wet, mix, !cleanEachStroke and mixing). Missing fields keep their current values.',\n properties: {\n pickup: { type: 'number', minimum: 0, maximum: 1, description: 'Pick-up ratio 0..1: how much of the paint under each dab the brush lifts and carries along (0: none, a dry brush).' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"Mix ratio 0..1: how much of the carried paint goes into what the brush lays down, against the brush's own colour (scaled by pickup).\" },\n dirty: { type: 'boolean', description: 'Dirty brush: keep the carried paint after the stroke, so the next stroke starts with the last colour it touched (needs pickup above 0). clean_mixer_brush wipes it.' },\n mixing: MIXING,\n multicolor: {\n type: 'object',\n description: 'Multi-coloured brush (the older form of paint, the brush\\'s own gradient: tip is across-tip, dab is distance in dabs or random). Read back from paint when it has a multi-colour equivalent. Missing fields keep their current values.',\n properties: {\n mode: { type: 'string', enum: [...MULTI_COLOR_MODES], description: 'off; tip: the colours lie side by side across the tip (stripes along the stroke, like a brush loaded with several paints); dab: the colour changes along the stroke.' },\n colors: { type: 'array', maxItems: 8, items: { ...COLOR }, description: '2..8 colours #rrggbb; fewer than 2 uses the paint colour and the secondary colour.' },\n order: { type: 'string', enum: ['sequence', 'random'], description: 'dab mode: sequence runs through the colours in order, blending between them; random picks one per run of dabs (seeded).' },\n length: { type: 'integer', minimum: 1, maximum: 500, description: 'dab mode: dabs per colour (default 12).' },\n softness: { type: 'number', minimum: 0, maximum: 1, description: 'How softly neighbouring colours blend: 0 hard stripes or steps, 1 a smooth gradient.' },\n },\n additionalProperties: false,\n },\n },\n additionalProperties: false,\n} as const;\n\nconst BRUSH_PAINT = {\n description: \"The brush's own gradient paint, used instead of the editor's paint (null: the editor's paint). It is what the multi-colour settings became; stops may be \\\"paint\\\" / \\\"secondary\\\" for the editor's two colours.\",\n anyOf: [PAINT_GRADIENT_JSON_SCHEMA, { type: 'null', description: \"No gradient of its own: the editor's paint.\" }],\n} as const;\n\nconst BLENDER = {\n type: 'object',\n description: 'Blender engine settings. Missing fields keep their current values.',\n properties: {\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'How far each pixel under the tip moves towards the mix of the paint near it, per dab.' },\n distance: { type: 'number', minimum: 0, maximum: 1, description: 'Blend distance: how far away colours are mixed in, as a fraction of the tip diameter (0.35 default; higher blends wider and softer).' },\n mixing: MIXING,\n },\n additionalProperties: false,\n} as const;\n\nconst SMUDGE = {\n type: 'object',\n description: 'Smudge engine settings. Missing fields keep their current values.',\n properties: {\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'How strongly the dragged paint covers what is under the tip, per dab.' },\n length: { type: 'number', minimum: 0, maximum: 1, description: 'How far paint is dragged: the share of the carried paint kept from dab to dab (0.85 default; 1 drags it the whole stroke).' },\n mixing: { ...MIXING, description: 'rgb (default): smeared colours average; pigment: they mix like paint.' },\n },\n additionalProperties: false,\n} as const;\n\nconst WET_BRUSH = {\n type: 'object',\n description: 'Wet engine settings (watercolour, gouache and the wet tools). Missing fields keep their current values.',\n properties: {\n tool: {\n type: 'string',\n enum: [...WET_TOOLS],\n description:\n 'paint: water and pigment (picks up and mixes wet paint by behavior.pickup / mix); water: clear water (with rewet it lifts dried paint); dry: a hair dryer, takes water; blow: pushes the water along the stroke; blend: evens out wet pigment; smudge: drags wet paint; blot: a tissue, takes wet paint up (erase wet).',\n },\n medium: { type: 'string', enum: [...WET_MEDIA], description: \"The medium: watercolor, gouache or oil. A layer takes the medium's wet settings the first time it holds wet paint (oil: thick, barely flows, stays wet long and blends into the wet paint it meets).\" },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'Paint loading: 1 never runs out; lower, the brush runs dry along a long stroke (each stroke reloads).' },\n thinner: { type: 'number', minimum: 0, maximum: 1, description: 'Thinner (water or solvent): more transparent, runnier paint.' },\n water: { type: 'number', minimum: 0, maximum: 1, description: 'Wetness: water laid per stroke, 0.2 a damp brush .. 1 a dripping one.' },\n pigment: { type: 'number', minimum: 0, maximum: 1, description: 'Pigment concentration: 0 clear water .. 1 straight from the tube.' },\n body: { type: 'number', minimum: 0, maximum: 1, description: 'Body: 0 a transparent watercolour (glazes) .. 1 opaque gouache (hides what is under it).' },\n strength: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of dry, blow, blend, smudge and blot.' },\n rewet: { type: 'number', minimum: 0, maximum: 1, description: 'How much dried paint the water lifts again: 0 none (staining) .. 1.' },\n },\n additionalProperties: false,\n} as const;\n\nconst IMPASTO_BRUSH = {\n type: 'object',\n description: 'Impasto engine settings (thick paint with relief, lit by set_lighting). Missing fields keep their current values.',\n properties: {\n tool: {\n type: 'string',\n enum: [...IMPASTO_TOOLS],\n description: 'brush: thick paint with bristle ridges along the stroke; tube: blobs squeezed from the tube; knife: a palette knife that scrapes the relief flat and smears the paint; roller: a flat stippled band; gloop: a thick, glossy, rounded bead.',\n },\n thickness: { type: 'number', minimum: 0, maximum: 1, description: 'How far the paint stands up.' },\n gloss: { type: 'number', minimum: 0, maximum: 1, description: 'Shine: 0 matte .. 1 wet-looking gloss.' },\n metallic: { type: 'number', minimum: 0, maximum: 1, description: 'Metallic paint: highlights take the paint colour (gold, copper, silver).' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'Paint loading: 1 never runs out; lower, the stroke thins and fades as the brush empties.' },\n thinner: { type: 'number', minimum: 0, maximum: 1, description: 'Thinner: flatter, more transparent paint.' },\n drag: { type: 'number', minimum: 0, maximum: 1, description: 'Knife and brush: how much of the paint under the tool is dragged along.' },\n },\n additionalProperties: false,\n} as const;\n\nconst PARTICLE_BRUSH = {\n type: 'object',\n description: 'Particle engine settings (procedural brushes). Missing fields keep their current values.',\n properties: {\n kind: { type: 'string', enum: [...PARTICLE_KINDS], description: 'string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade.' },\n render: { type: 'string', enum: [...PARTICLE_RENDERS], description: 'dot: a mark at each step; line: each particle draws a continuous line; sequence: beads spaced along each path.' },\n count: { type: 'integer', minimum: 1, maximum: 64, description: 'Particles (flow: how many are thrown per tip of travel).' },\n size: { type: 'number', minimum: 0, maximum: 1, description: 'Mark size as a fraction of the tip size.' },\n spread: { type: 'number', minimum: 0, maximum: 1, description: 'How far apart the particles start, as a fraction of the tip size.' },\n stiffness: { type: 'number', minimum: 0, maximum: 1, description: 'Spring and string stiffness: stiff follows closely, soft swings wide.' },\n damping: { type: 'number', minimum: 0, maximum: 1, description: 'How quickly motion dies down.' },\n speed: { type: 'number', minimum: 0, maximum: 1, description: 'Orbit and flow speed.' },\n gravity: { type: 'number', minimum: -1, maximum: 1, description: 'Pull downwards (negative: upwards).' },\n life: { type: 'number', minimum: 0, maximum: 1, description: 'Flow particles: how long they live.' },\n interval: { type: 'number', minimum: 0, maximum: 1, description: 'Sequence: spacing of the beads.' },\n },\n additionalProperties: false,\n} as const;\n\nconst BRISTLE_BRUSH = {\n type: 'object',\n description: 'Bristle engine settings: a tuft of bristles, each its own line. Missing fields keep their current values.',\n properties: {\n count: { type: 'integer', minimum: 3, maximum: 128, description: 'Bristles in the tuft.' },\n stiffness: { type: 'number', minimum: 0, maximum: 1, description: 'Stiff bristles follow the tip; soft ones lag and bend into curves at turns.' },\n splay: { type: 'number', minimum: 0, maximum: 1, description: 'How far pressure spreads the tuft.' },\n thickness: { type: 'number', minimum: 0, maximum: 1, description: \"Each bristle's line width as a fraction of the tip size.\" },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'Paint per bristle: 1 never runs dry; lower, bristles run out along the stroke (dry brush).' },\n jitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random variation between bristles.' },\n },\n additionalProperties: false,\n} as const;\n\nconst STICKER_BRUSH = {\n type: 'object',\n description: 'Sticker engine settings: stamps sprayed along the stroke as editable stickers. Missing fields keep their current values.',\n properties: {\n stamps: { type: 'array', minItems: 1, maxItems: 9, items: { type: 'string', enum: [...STAMP_KINDS], description: 'A stamp.' }, description: 'The stamps to spray (one picked at random per sticker).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Stickers per distance travelled.' },\n sizeJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random size change.' },\n rotationJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random turn as a fraction of ±180°.' },\n scatter: { type: 'number', minimum: 0, maximum: 2, description: 'How far from the path stickers land, in tip diameters.' },\n colorJitter: { type: 'number', minimum: 0, maximum: 1, description: 'Random colour change around the paint colour.' },\n },\n additionalProperties: false,\n} as const;\n\nconst BRUSH_FIELDS = {\n name: { type: 'string', minLength: 1, maxLength: 80, description: 'Display name.' },\n category: { type: 'string', enum: [...PAINT_BRUSH_CATEGORIES], description: 'Library category.' },\n engine: {\n type: 'string',\n enum: [...PAINT_BRUSH_ENGINES],\n description: 'dab: paints dabs (with behavior.pickup or gradient paint it paints dab by dab, lifting and mixing paint); eraser: removes paint; mixer: the Mixer Brush (a reservoir plus picked-up paint); blender: mixes the colours under and near the tip; smudge: drags paint along the stroke; wet: watercolour, gouache, oil and the wet tools on a simulated wet layer (see wet); impasto: thick paint with relief (see impasto); particle: procedural particle brushes (see particle); bristle: a tuft of bristles that bend and splay (see bristle); sticker: sprays editable sticker stamps (see sticker).',\n },\n tip: TIP,\n grain: { type: 'number', minimum: 0, maximum: 1, description: 'Paper grain response: 0 ignores the grain, 1 catches only its peaks at light pressure (pencil, pastel, charcoal). Multiplied by the paper depth.' },\n pressureSmoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Pressure smoothing, separate from the tip position smoothing.' },\n rotation: { type: 'string', enum: [...BRUSH_ROTATIONS], description: 'What turns the tip: fixed (tip.angle), stroke (follows the direction of travel) or barrel (the pen barrel rotation, twist).' },\n dynamics: DYNAMICS,\n mixer: MIXER,\n loadsColor: { type: 'boolean', description: 'Mixer engine: load the colour each stroke (false for a mixer that only moves paint).' },\n behavior: BEHAVIOR,\n blender: BLENDER,\n smudge: SMUDGE,\n wet: WET_BRUSH,\n impasto: IMPASTO_BRUSH,\n particle: PARTICLE_BRUSH,\n bristle: BRISTLE_BRUSH,\n sticker: STICKER_BRUSH,\n paint: BRUSH_PAINT,\n} as const;\n\nconst POINTS = {\n type: 'array',\n minItems: 1,\n maxItems: 20000,\n description: 'The path in document pixels (raw samples; the brush smooths them). One point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in pixels.' },\n y: { type: 'number', description: 'Document y in pixels.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure 0..1 (default 1).' },\n t: { type: 'number', description: 'Time in milliseconds (optional; drives velocity dynamics, e.g. an ink pen thinning on fast strokes).' },\n tiltX: { type: 'number', minimum: -90, maximum: 90, description: 'Pen tilt in degrees towards +x (Pointer Events tiltX).' },\n tiltY: { type: 'number', minimum: -90, maximum: 90, description: 'Pen tilt in degrees towards +y (Pointer Events tiltY).' },\n altitude: { type: 'number', minimum: 0, maximum: 1.5708, description: 'Pen altitude in radians: 1.5708 upright, 0 lying flat (overrides tiltX / tiltY).' },\n azimuth: { type: 'number', minimum: 0, maximum: 6.2832, description: 'Where the pen leans, radians clockwise from +x (with altitude).' },\n twist: { type: 'number', minimum: 0, maximum: 359, description: 'Barrel rotation in degrees (brushes with rotation barrel).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n} as const;\n\nfunction loadResult(load: MixerLoad) {\n const hex = (c: readonly number[] | null) => (c ? '#' + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('') : null);\n return { reservoir: hex(load.reservoir), amount: Math.round(load.amount * 1000) / 1000, pickup: hex(load.pickup) };\n}\n\nfunction brushSummary(b: PaintBrush) {\n return {\n gradient: b.paint !== null,\n id: b.id,\n name: b.name,\n category: b.category,\n engine: b.engine,\n size: b.tip.size,\n grain: b.grain,\n rotation: b.rotation,\n wetCapable: isWetCapable(b),\n pickup: b.behavior.pickup,\n dirty: b.behavior.dirty,\n multicolor: b.behavior.multicolor.mode,\n ...(b.engine === 'wet' ? { wetTool: b.wet.tool, medium: b.wet.medium } : {}),\n ...(b.engine === 'impasto' ? { impastoTool: b.impasto.tool } : {}),\n builtin: Boolean(b.builtin),\n };\n}\n\n/** Agent tools for one paint editor. */\nexport function createPaintEditorToolProvider(controller: PaintEditorController, options: PaintEditorToolProviderOptions = {}): AgentToolProvider {\n const c = controller;\n const tools: AgentTool[] = [];\n const add = (t: AgentTool) => tools.push(t);\n /** Runs `command` through the director when there is one (its error becomes the tool error), else `direct`. */\n const act = async <T>(command: PaintAgentCommand, direct: () => T): Promise<T> => {\n if (!options.director) return direct();\n const [r] = await options.director.perform(options.actor ?? { id: 'agent', name: 'Agent' }, [command]);\n if (!r || !r.ok) throw new Error(r && !r.ok ? r.message : 'The director did not run the command.');\n return r.result as T;\n };\n\n // --- Canvas ------------------------------------------------------------------\n\n add({\n name: 'get_canvas_info',\n title: 'Get the canvas',\n description:\n 'Reads the canvas: size in pixels and dpi, the paper (preset, colour, grain depth), the active layer, the active brush id, the paint colour, the view (zoom, rotation, mirror), the hand, how many strokes are recorded, the undo / redo labels, and the blend modes. Call it first.',\n inputSchema: EMPTY,\n annotations: { title: 'Get the canvas', ...READ },\n handler: () => {\n const s = c.getState();\n const d = s.document;\n return {\n width: d.canvas.width,\n height: d.canvas.height,\n dpi: d.canvas.dpi,\n paper: { preset: d.paper.preset, name: d.paper.name, color: d.paper.color, depth: d.paper.depth },\n activeLayerId: d.activeLayerId,\n layerCount: c.listLayers().length,\n brushId: s.brush.id,\n color: s.color,\n secondaryColor: s.secondaryColor,\n view: { zoom: s.view.zoom, rotation: s.view.rotation, mirror: s.view.mirror },\n hand: s.hand,\n strokes: d.strokes.length,\n undo: s.undoLabel,\n redo: s.redoLabel,\n blendModes: BLEND_MODES.map((m) => m.id),\n wet: s.wet.wet,\n lighting: c.getLighting(),\n };\n },\n });\n\n add({\n name: 'new_canvas',\n title: 'Start a new canvas',\n description:\n 'Replaces the document with a blank canvas: one empty layer over the paper. Size and dpi default to the current ones, the paper to the current paper (or a preset id from list_papers). One undo step (undo brings the old painting back). Returns the new size, paper and layer id.',\n inputSchema: {\n type: 'object',\n properties: {\n width: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Width in pixels (default the current width).' },\n height: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Height in pixels (default the current height).' },\n dpi: { type: 'number', minimum: 1, maximum: 10000, description: 'Pixels per inch for print sizes and PSD (default the current dpi).' },\n paper: { type: 'string', description: 'Paper preset id from list_papers (default the current paper).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Start a new canvas', ...DESTRUCTIVE },\n handler: (a) => {\n const d = c.newCanvas({ width: a.width, height: a.height, dpi: a.dpi, paper: a.paper });\n return { width: d.canvas.width, height: d.canvas.height, dpi: d.canvas.dpi, paper: d.paper.preset, layerId: d.activeLayerId };\n },\n });\n\n add({\n name: 'set_canvas',\n title: 'Resize the canvas',\n description:\n 'Changes the canvas size (pixels stay where they are, anchored at the top-left; anything outside is kept on the layers but not shown) and / or its dpi. One undo step. Returns the canvas.',\n inputSchema: {\n type: 'object',\n properties: {\n width: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Width in pixels (default unchanged).' },\n height: { type: 'integer', minimum: 1, maximum: MAX_CANVAS_SIDE, description: 'Height in pixels (default unchanged).' },\n dpi: { type: 'number', minimum: 1, maximum: 10000, description: 'Pixels per inch (default unchanged).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Resize the canvas', ...SET },\n handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi }),\n });\n\n // --- Paper -------------------------------------------------------------------\n\n add({\n name: 'list_papers',\n title: 'List papers',\n description:\n 'Lists the built-in paper library (generated grain, no textures): id, name, family, grain pattern, colour, grain depth and the wet-media properties (absorption, permeability, roughness). Use an id with set_paper or new_canvas.',\n inputSchema: EMPTY,\n annotations: { title: 'List papers', ...READ },\n handler: () => ({\n papers: PAPER_LIBRARY.map((p) => ({ id: p.preset, name: p.name, family: p.family, pattern: p.pattern, color: p.color, depth: p.depth, absorption: p.absorption, permeability: p.permeability, roughness: p.roughness })),\n families: [...PAPER_FAMILIES],\n patterns: [...GRAIN_PATTERNS],\n }),\n });\n\n add({\n name: 'get_paper',\n title: 'Get the paper',\n description: 'Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness).',\n inputSchema: EMPTY,\n annotations: { title: 'Get the paper', ...READ },\n handler: () => JSON.parse(JSON.stringify(c.document.paper)),\n });\n\n const tuning = {\n type: 'object',\n properties: {\n contrast: { type: 'number', minimum: 0, maximum: 3, description: 'Height contrast around the middle (default 1).' },\n invert: { type: 'boolean', description: 'Swap peaks and valleys.' },\n },\n additionalProperties: false,\n } as const;\n\n add({\n name: 'set_paper',\n title: 'Set the paper',\n description:\n 'Changes the paper under the whole painting: a preset from list_papers and / or its settings. Grain depth decides how much grain-responsive brushes (pencil, pastel, charcoal) skip the valleys; changing it re-renders the paper but not strokes already painted. One undo step. Returns the paper.',\n inputSchema: {\n type: 'object',\n properties: {\n preset: { type: 'string', description: 'Paper id from list_papers (its settings, then the other fields here on top).' },\n color: { ...COLOR, description: 'Paper colour #rrggbb.' },\n depth: { type: 'number', minimum: 0, maximum: 1, description: 'Grain depth 0..1 (0: brushes ignore the grain).' },\n scale: { type: 'number', minimum: 0.25, maximum: 8, description: 'Grain scale: 1 natural, 2 twice as coarse.' },\n seed: { type: 'integer', minimum: 0, description: 'Grain seed: another sheet of the same paper.' },\n pattern: { type: 'string', enum: [...GRAIN_PATTERNS], description: 'Grain pattern.' },\n tiling: { type: 'string', enum: ['repeat', 'mirror'], description: 'How the grain tile repeats (mirror hides seams).' },\n shading: { type: 'number', minimum: 0, maximum: 1, description: 'How much the grain shades the paper colour.' },\n dry: { ...tuning, description: 'Height map tuning for dry media.' },\n wet: { ...tuning, description: 'Height map tuning for wet media (the grain water and pigment follow).' },\n absorption: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how fast the paper soaks up water (high: washes stop spreading sooner).' },\n permeability: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how freely water flows over the fibres (low: more ragged, slower spreading).' },\n roughness: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how much the grain pins wet edges and catches granulating pigment.' },\n metallic: { type: 'number', minimum: 0, maximum: 1, description: 'Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light (see set_lighting). 0 by default.' },\n name: { type: 'string', minLength: 1, description: 'Display name.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the paper', ...SET },\n handler: (a) => c.setPaper(a),\n });\n\n // --- Layers ------------------------------------------------------------------\n\n add({\n name: 'list_layers',\n title: 'List layers',\n description:\n 'Lists the layers top first: id, name, type (pixel or group), visibility, opacity, blend, clipping, locks, whether it is active, the painted bounds in document pixels (null when empty), parent and depth.',\n inputSchema: EMPTY,\n annotations: { title: 'List layers', ...READ },\n handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId }),\n });\n\n add({\n name: 'add_layer',\n title: 'Add a layer',\n description: 'Adds an empty pixel layer (or a group) above the active layer, or above aboveId, or at the top of parentId; it becomes the active layer. One undo step. Returns the layer.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', minLength: 1, description: 'Layer name (default \"Layer n\").' },\n type: { type: 'string', enum: ['pixel', 'group'], description: 'pixel (default) or group.' },\n aboveId: { type: 'string', description: 'Put it directly above this layer (default the active layer).' },\n parentId: { type: 'string', description: 'Put it at the top of this group instead.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer', ...WRITE },\n handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId }),\n });\n\n add({\n name: 'update_layer',\n title: 'Change a layer',\n description: 'Changes a layer: name, visibility, opacity, blend mode, clipping to the layer below, locks. One undo step. Returns the layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Layer id from list_layers.' },\n name: { type: 'string', minLength: 1, description: 'New name.' },\n visible: { type: 'boolean', description: 'Show or hide.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n blend: {\n type: 'string',\n enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'],\n description: 'Blend mode (pass-through for groups only). Painterly modes: watercolor (the layer glazes like a transparent wash), tint (recolours keeping lightness), highlight (adds soft coloured light), shadow (shades with ambient fill); see list_blend_modes.',\n },\n clipped: { type: 'boolean', description: 'Clip to the layer below (paint only shows where it has paint).' },\n locks: {\n type: 'object',\n description: 'Locks to change (others keep their values).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting.' },\n transparency: { type: 'boolean', description: 'Paint only where the layer already has paint.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'All of the above, plus property changes and removal.' },\n },\n additionalProperties: false,\n },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer', ...SET },\n handler: ({ layerId, ...u }) => c.updateLayer(layerId, u as { blend?: BlendMode }),\n });\n\n add({\n name: 'list_blend_modes',\n title: 'List blend modes',\n description:\n 'Lists every layer and brush blend mode: id, label, group (standard Photoshop modes, pigment, or painterly), what the pigment and painterly modes do, and the standard mode PSD export writes in their place. Changes nothing. Read it before choosing a painterly mode for update_layer or a brush tip.',\n inputSchema: EMPTY,\n annotations: { title: 'List blend modes', ...READ },\n handler: () => ({\n modes: BLEND_MODES.map((m) => ({\n id: m.id,\n label: m.label,\n group: m.painterly ? 'painterly' : m.id === 'pigment' ? 'pigment' : 'standard',\n ...(m.description ? { description: m.description } : {}),\n psd: standardBlendMode(m.id),\n })),\n }),\n });\n\n add({\n name: 'set_active_layer',\n title: 'Choose the active layer',\n description: 'Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Layer id from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Choose the active layer', ...SET },\n handler: ({ layerId }) => {\n c.setActiveLayer(layerId);\n return c.describeLayer(c.getLayer(layerId)!);\n },\n });\n\n add({\n name: 'remove_layer',\n title: 'Remove a layer',\n description: 'Removes a layer (a group with its children). One undo step. Returns the removed id and the new active layer.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Layer id from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Remove a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => c.removeLayer(layerId),\n });\n\n add({\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'Layer id from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId }) => c.duplicateLayer(layerId),\n });\n\n add({\n name: 'move_layer',\n title: 'Reorder a layer',\n description: 'Moves a layer to a position in its group or another (0 is the bottom). One undo step. Returns the layer with its new parent.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Layer id from list_layers.' },\n index: { type: 'integer', minimum: 0, description: 'New position among its siblings, 0 = bottom.' },\n parentId: { type: ['string', 'null'], description: 'Group id to move into, or null for the top level (default its current parent).' },\n },\n required: ['layerId', 'index'],\n additionalProperties: false,\n },\n annotations: { title: 'Reorder a layer', ...WRITE },\n handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId),\n });\n\n add({\n name: 'merge_layer_down',\n title: 'Merge a layer down',\n description: 'Merges a pixel layer into the pixel layer directly below it, which keeps its name, opacity and blend. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { type: 'string', description: 'The upper layer, from list_layers.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: ({ layerId }) => c.mergeDown(layerId),\n });\n\n add({\n name: 'fill_layer',\n title: 'Fill a layer',\n description:\n 'Fills a whole pixel layer with a colour: a flat block-in, or with grain above 0 a fill that follows the paper grain like pastel or charcoal rubbed across the sheet (pressure decides how far it reaches into the valleys). Good for a ground colour or a tonal layer to paint over. One undo step. Returns the layer id and the filled bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n color: { ...COLOR, description: 'Fill colour #rrggbb (default the paint colour).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity 0..1 (default 1).' },\n grain: { type: 'number', minimum: 0, maximum: 1, description: 'Paper grain response 0..1 (default 0: flat).' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'With grain: how far into the valleys, 0..1 (default 0.5).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal; pigment mixes like paint, watercolor glazes; see list_blend_modes).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill a layer', ...WRITE },\n handler: (a) => act({ kind: 'fill', fill: a }, () => c.fillLayer(a)),\n });\n\n add({\n name: 'clear_layer',\n title: 'Clear a layer',\n description: 'Removes every pixel of a pixel layer. One undo step. Returns the layer id and the bounds that were cleared (null if it was empty).',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, additionalProperties: false },\n annotations: { title: 'Clear a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => c.clearLayer(layerId),\n });\n\n // --- Brushes -----------------------------------------------------------------\n\n add({\n name: 'list_brushes',\n title: 'List brushes',\n description:\n 'Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.',\n inputSchema: EMPTY,\n annotations: { title: 'List brushes', ...READ },\n handler: () => {\n const s = c.getState();\n return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(c.getBrush(s.brush.id)?.builtin), brushes: s.brushes.map(brushSummary) };\n },\n });\n\n add({\n name: 'get_brush',\n title: 'Get a brush',\n description:\n \"Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer, behavior: pick-up / mix / dirty, blender, smudge, its own gradient paint as paint, null when it paints the editor's paint). Without id, the active brush as edited. Also returns strokePaint: what a stroke with it would paint now (its gradient, else the editor's paint from set_paint).\",\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Brush id from list_brushes (default the active brush).' } }, additionalProperties: false },\n annotations: { title: 'Get a brush', ...READ },\n handler: ({ id }) => {\n const st = c.getState();\n if (id === undefined) return { ...brushToJSON(st.brush), strokePaint: st.brush.paint ?? st.paint };\n const b = c.getBrush(id);\n if (!b) throw new Error(`Unknown brush \"${id}\". Call list_brushes for the ids.`);\n return { ...brushToJSON(b), builtin: Boolean(b.builtin), strokePaint: b.paint ?? st.paint };\n },\n });\n\n add({\n name: 'set_brush',\n title: 'Set the brush',\n description:\n 'Chooses the active brush (id) and / or changes its settings; strokes without a brush use it. Editor state, not an undo step. Changes stay on the active brush until save_brush stores them. Returns the active brush.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Brush id from list_brushes to start from.' }, ...BRUSH_FIELDS }, additionalProperties: false },\n annotations: { title: 'Set the brush', ...SET },\n handler: (a) => brushToJSON(c.setBrush(a)),\n });\n\n add({\n name: 'save_brush',\n title: 'Save a brush',\n description: 'Saves the active brush (with its changes) into the user library under a new or existing user id. Built-in ids are refused. Library state, not an undo step. Returns the saved brush.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$', description: 'Library id: lowercase letters, digits and dashes.' },\n name: { type: 'string', minLength: 1, maxLength: 80, description: 'Display name (default the brush name).' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n annotations: { title: 'Save a brush', ...SET },\n handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name })),\n });\n\n add({\n name: 'remove_brush',\n title: 'Remove a saved brush',\n description: 'Removes a user brush from the library (built-in brushes cannot be removed). Library state, not an undo step. Returns the removed id.',\n inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'User brush id from list_brushes.' } }, required: ['id'], additionalProperties: false },\n annotations: { title: 'Remove a saved brush', ...DESTRUCTIVE },\n handler: ({ id }) => {\n c.removeBrush(id);\n return { removed: id };\n },\n });\n\n add({\n name: 'export_brushes',\n title: 'Export brushes',\n description: 'Returns a brush library file (JSON: format pixerate-brushes, version 1, brushes) of the given brushes, or of every user brush. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: { ids: { type: 'array', items: { type: 'string', description: 'Brush id.' }, description: 'Brush ids from list_brushes (default every user brush).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Export brushes', ...READ },\n handler: ({ ids }) => c.exportBrushes(ids),\n });\n\n add({\n name: 'import_brushes',\n title: 'Import brushes',\n description:\n 'Adds the brushes of a brush library file (as export_brushes returns) to the user library. Taken ids are skipped unless replace is true; built-in ids are always skipped. Library state, not an undo step. Returns the ids added and skipped and any problems.',\n inputSchema: {\n type: 'object',\n properties: {\n library: { type: 'object', description: 'The library file: {format: \"pixerate-brushes\", version: 1, brushes: [...]}.' },\n replace: { type: 'boolean', description: 'Replace user brushes with the same id (default false).' },\n },\n required: ['library'],\n additionalProperties: false,\n },\n annotations: { title: 'Import brushes', ...WRITE },\n handler: ({ library, replace }) => c.importBrushes(library, { replace }),\n });\n\n add({\n name: 'render_brush_thumbnail',\n title: 'Render a brush thumbnail',\n description: 'Renders a brush as a PNG: a test stroke (an S-curve swelling and fading in pressure) on the current paper, so you can see what it does before painting. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Brush id from list_brushes (default the active brush).' },\n width: { type: 'integer', minimum: 16, maximum: 512, description: 'Width in pixels (default 160).' },\n height: { type: 'integer', minimum: 16, maximum: 512, description: 'Height in pixels (default 48).' },\n color: { ...COLOR, description: 'Stroke colour (default the paint colour).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Render a brush thumbnail', ...READ },\n handler: ({ id, width, height, color }) => {\n const t = c.brushThumbnail(id ?? c.getState().brush.id, { width, height, color });\n return {\n content: [\n { type: 'image', data: bytesToBase64(encodePng(t.width, t.height, t.data)), mimeType: 'image/png' },\n { type: 'text', text: `Brush ${id ?? c.getState().brush.id} ${t.width}×${t.height}` },\n ],\n };\n },\n });\n\n // --- Colour and the Mixer Brush ----------------------------------------------\n\n add({\n name: 'set_color',\n title: 'Set the paint colour',\n description: 'Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Editor state, not an undo step. Returns both colours.',\n inputSchema: {\n type: 'object',\n properties: { color: { ...COLOR, description: 'Paint colour #rrggbb.' }, secondaryColor: { ...COLOR, description: 'Second colour #rrggbb.' } },\n additionalProperties: false,\n },\n annotations: { title: 'Set the paint colour', ...SET },\n handler: ({ color, secondaryColor }) => {\n if (color !== undefined) c.setColor(color);\n if (secondaryColor !== undefined) c.setSecondaryColor(secondaryColor);\n const s = c.getState();\n return { color: s.color, secondaryColor: s.secondaryColor };\n },\n });\n\n add({\n name: 'set_paint',\n title: 'Set the paint (colour or gradient)',\n description:\n 'Sets what strokes paint with: one colour, or gradient paint that changes colour as the stroke goes. Gradient paint: 2..16 stops sorted by offset (#rrggbb, or \"paint\" / \"secondary\" for the two editor colours), space pigment (default: mixes like paint) / oklab / srgb, and map: distance (default: the colour changes as the stroke is dragged out, 300 px then mirrored back), pressure, tilt, velocity, direction, time, across-tip or random. Every stroking brush samples it per dab (dab, mixer, watercolour and gouache, oil and impasto, particle, bristle, sticker); erasers, blenders and smudges ignore it, and a brush with its own gradient (get_brush paint) paints that instead. set_color goes back to one colour. Editor state, not an undo step. Returns paint, color and secondaryColor.',\n inputSchema: { type: 'object', properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ['paint'], additionalProperties: false },\n annotations: { title: 'Set the paint (colour or gradient)', ...SET },\n handler: ({ paint }) => {\n const p = c.setPaint(paint);\n const s = c.getState();\n return { paint: p, color: s.color, secondaryColor: s.secondaryColor };\n },\n });\n\n add({\n name: 'pick_color',\n title: 'Pick a colour',\n description: 'Reads the colour at a document pixel: of the painting with its paper, or of one layer (with its alpha). Changes nothing. Returns #rrggbb and alpha 0..1.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in pixels.' },\n y: { type: 'number', description: 'Document y in pixels.' },\n layerId: { type: 'string', description: 'Pick from this layer only (default the painting as shown).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n annotations: { title: 'Pick a colour', ...READ },\n handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId }),\n });\n\n add({\n name: 'get_mixer_brush',\n title: 'Get the Mixer Brush load',\n description:\n \"Reads what the brush carries between strokes: reservoir #rrggbb or null (the Mixer Brush's loaded paint), amount 0..1 left, pickup #rrggbb or null (paint picked up from the canvas, kept by dirty brushes and a Mixer Brush that does not clean each stroke). Call before painting with mixer-brush when its loadEachStroke is false, or with a dirty brush.\",\n inputSchema: EMPTY,\n annotations: { title: 'Get the Mixer Brush load', ...READ },\n handler: () => loadResult(c.getState().mixerLoad),\n });\n\n add({\n name: 'load_mixer_brush',\n title: 'Load the Mixer Brush',\n description: 'Fills the Mixer Brush reservoir with a colour (amount 1), keeping what it picked up. Editor state, not an undo step. Returns the load.',\n inputSchema: { type: 'object', properties: { color: { ...COLOR, description: 'Colour #rrggbb (default the paint colour).' } }, additionalProperties: false },\n annotations: { title: 'Load the Mixer Brush', ...SET },\n handler: ({ color }) => loadResult(c.loadMixerBrush(color)),\n });\n\n add({\n name: 'clean_mixer_brush',\n title: 'Clean the Mixer Brush',\n description: 'Cleans the brush: empties the picked-up paint a dirty brush or the Mixer Brush carries (and with emptyReservoir the Mixer Brush paint too). Editor state, not an undo step. Returns the load.',\n inputSchema: { type: 'object', properties: { emptyReservoir: { type: 'boolean', description: 'Also drop the loaded paint (default false).' } }, additionalProperties: false },\n annotations: { title: 'Clean the Mixer Brush', ...SET },\n handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true)),\n });\n\n // --- Painting ----------------------------------------------------------------\n\n add({\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer’s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.',\n inputSchema: {\n type: 'object',\n properties: {\n points: POINTS,\n layerId: LAYER_ID,\n brushId: { type: 'string', description: 'Brush id from list_brushes (default the active brush).' },\n brush: { type: 'object', description: 'Brush changes for this stroke only.', properties: BRUSH_FIELDS, additionalProperties: false },\n color: { ...COLOR, description: 'Paint colour #rrggbb (default the paint colour).' },\n paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: \"What this stroke paints with instead of the editor's paint: a colour, or gradient paint that changes colour as the stroke goes (by distance, pressure, tilt, speed, direction, time, across the tip or at random). Wins over the brush's own gradient.\" },\n seed: { type: 'integer', minimum: 0, description: 'Jitter seed (default a new one); the same seed and points give the same marks.' },\n snapToPerspective: { type: 'boolean', description: 'Snap the points to the perspective guide, as a pen stroke would (default false).' },\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: async (a) => {\n await c.wetIdle();\n const stroke = { points: a.points as PenPoint[], layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };\n const r = await act({ kind: 'stroke', stroke }, () => c.paintStroke(stroke));\n return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;\n },\n });\n\n add({\n name: 'list_strokes',\n title: 'List recorded strokes',\n description:\n 'Lists the recorded strokes, oldest first (every stroke is kept as data): id, layer, brush id and name, colour, point count, bounds and seed. With ids, also the points. Use the ids with replay_strokes.',\n inputSchema: {\n type: 'object',\n properties: {\n ids: { type: 'array', items: { type: 'string', description: 'Stroke id.' }, description: 'Only these strokes, with their points.' },\n last: { type: 'integer', minimum: 1, maximum: 1000, description: 'Only the most recent n strokes (default 100).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'List recorded strokes', ...READ },\n handler: ({ ids, last }) => {\n const all = c.getStrokes();\n const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(last ?? 100));\n if (ids) for (const id of ids) if (!all.some((s) => s.id === id)) throw new Error(`Unknown stroke \"${id}\". Call list_strokes without ids for the ids.`);\n return {\n total: all.length,\n strokes: picked.map((s) => {\n const xs = s.points.map((p) => p.x);\n const ys = s.points.map((p) => p.y);\n const pad = s.brush.tip.size / 2;\n return {\n id: s.id,\n layerId: s.layerId,\n brushId: s.brush.id,\n brushName: s.brush.name,\n color: s.color,\n points: ids ? s.points : s.points.length,\n seed: s.seed,\n copies: s.copies?.length ?? 0,\n bounds: { x: Math.floor(Math.min(...xs) - pad), y: Math.floor(Math.min(...ys) - pad), width: Math.ceil(Math.max(...xs) - Math.min(...xs) + 2 * pad), height: Math.ceil(Math.max(...ys) - Math.min(...ys) + 2 * pad) },\n };\n }),\n };\n },\n });\n\n add({\n name: 'replay_strokes',\n title: 'Replay strokes, varied',\n description:\n 'Paints recorded strokes again, moved, scaled, rotated, flipped, recoloured, with another brush or on another layer, repeat times (each repeat applies the variation again: a row, a fan, a spiral). Strokes replay exactly (same seed). All of it is one undo step. Returns the new stroke ids, the changed bounds and layer ids.',\n inputSchema: {\n type: 'object',\n properties: {\n strokeIds: { type: 'array', minItems: 1, maxItems: 500, items: { type: 'string', description: 'Stroke id.' }, description: 'Stroke ids from list_strokes.' },\n dx: { type: 'number', description: 'Move right by this many pixels per repeat (default 0).' },\n dy: { type: 'number', description: 'Move down by this many pixels per repeat (default 0).' },\n scale: { type: 'number', exclusiveMinimum: 0, maximum: 20, description: 'Scale per repeat about the origin; brush size scales too (default 1).' },\n rotate: { type: 'number', minimum: -360, maximum: 360, description: 'Rotate per repeat about the origin, degrees clockwise (default 0).' },\n flipX: { type: 'boolean', description: 'Mirror left to right about the origin (default false).' },\n origin: {\n type: 'object',\n description: 'Centre for scale, rotate and flip (default the centre of the strokes).',\n properties: { x: { type: 'number', description: 'Document x.' }, y: { type: 'number', description: 'Document y.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n pressureScale: { type: 'number', minimum: 0, maximum: 4, description: 'Multiply every pressure (default 1).' },\n color: { ...COLOR, description: 'Recolour (default each stroke’s colour).' },\n brushId: { type: 'string', description: 'Replay with this brush from list_brushes (default each stroke’s brush).' },\n layerId: { type: 'string', description: 'Paint on this layer (default each stroke’s layer).' },\n repeat: { type: 'integer', minimum: 1, maximum: 100, description: 'How many copies (default 1).' },\n },\n required: ['strokeIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Replay strokes, varied', ...WRITE },\n handler: (a) => {\n const replay = a as Parameters<PaintEditorController['replayStrokes']>[0];\n return act({ kind: 'replay', replay }, () => c.replayStrokes(replay));\n },\n });\n\n // --- View, hand, history -------------------------------------------------------\n\n add({\n name: 'set_view',\n title: 'Rotate, mirror or zoom the view',\n description: 'Changes the view only (the pixels never resample): zoom, rotation and mirroring, as an artist turns the paper or checks the drawing in a mirror. Editor state, not an undo step. Returns the view.',\n inputSchema: {\n type: 'object',\n properties: {\n zoom: { type: 'number', minimum: MIN_ZOOM, maximum: MAX_ZOOM, description: 'Screen pixels per document pixel.' },\n rotation: { type: 'number', minimum: -360, maximum: 360, description: 'Rotation in degrees clockwise.' },\n mirror: { type: 'boolean', description: 'Mirror left to right.' },\n reset: { type: 'boolean', description: 'Back to 100 %, no rotation, no mirror, centred (applied first).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Rotate, mirror or zoom the view', ...SET },\n handler: ({ reset, ...v }) => {\n if (reset) c.resetView();\n return c.setView(v);\n },\n });\n\n add({\n name: 'set_hand',\n title: 'Set the drawing hand',\n description: 'Says which hand holds the pen; palm rejection ignores touches on that side of the pen. Editor state, not an undo step. Returns the hand.',\n inputSchema: { type: 'object', properties: { hand: { type: 'string', enum: ['right', 'left'], description: 'right or left.' } }, required: ['hand'], additionalProperties: false },\n annotations: { title: 'Set the drawing hand', ...SET },\n handler: ({ hand }) => ({ hand: c.setHand(hand) }),\n });\n\n add({\n name: 'undo_paint',\n title: 'Undo',\n description: 'Undoes the last step (a stroke, a fill, a layer or paper change). Returns the label undone (null when there was nothing) and what can be undone and redone next.',\n inputSchema: EMPTY,\n annotations: { title: 'Undo', ...WRITE },\n handler: () => {\n const undone = c.undo();\n const s = c.getState();\n return { undone, nextUndo: s.undoLabel, nextRedo: s.redoLabel };\n },\n });\n\n add({\n name: 'redo_paint',\n title: 'Redo',\n description: 'Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.',\n inputSchema: EMPTY,\n annotations: { title: 'Redo', ...WRITE },\n handler: () => {\n const redone = c.redo();\n const s = c.getState();\n return { redone, nextUndo: s.undoLabel, nextRedo: s.redoLabel };\n },\n });\n\n // --- Output ------------------------------------------------------------------\n\n add({\n name: 'render_preview',\n title: 'Render a preview',\n description: 'Renders the painting (with its paper unless paper is false) or some layers as a PNG whose longest side is at most maxSide. Changes nothing. Look at it after each block of work.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerIds: { type: 'array', items: { type: 'string', description: 'Layer id.' }, description: 'Only these layers (default all).' },\n paper: { type: 'boolean', description: 'Include the paper (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Render a preview', ...READ },\n handler: ({ maxSide, layerIds, paper }) => {\n if (layerIds) for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer \"${id}\". Call list_layers for the ids.`);\n const p = c.previewPngBase64({ maxSide: maxSide ?? 512, layerIds, paper });\n return { content: [{ type: 'image', data: p.data, mimeType: 'image/png' }, { type: 'text', text: `Preview ${p.width}×${p.height}` }] };\n },\n });\n\n add({\n name: 'export_image',\n title: 'Export the painting',\n description: 'Exports the painting as PNG, JPEG or WebP (base64). JPEG always includes the paper; JPEG and WebP need a browser canvas. Changes nothing. Returns the data, its MIME type and size.',\n inputSchema: {\n type: 'object',\n properties: {\n format: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp'], description: 'File type (default image/png).' },\n quality: { type: 'number', minimum: 0, maximum: 1, description: 'JPEG / WebP quality 0..1 (default 0.92).' },\n scale: { type: 'number', exclusiveMinimum: 0, maximum: 1, description: 'Shrink by this factor (default 1).' },\n paper: { type: 'boolean', description: 'Include the paper (default true).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Export the painting', ...READ },\n handler: async ({ format, quality, scale, paper }) => {\n const bytes = await c.exportImage({ format, quality, scale, paper });\n return { mimeType: format ?? 'image/png', bytes: bytes.length, data: bytesToBase64(bytes) };\n },\n });\n\n add({\n name: 'export_psd',\n title: 'Export as PSD',\n description:\n 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked \"Paper\" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',\n inputSchema: EMPTY,\n annotations: { title: 'Export as PSD', ...READ },\n handler: () => {\n const r = c.exportPsd();\n return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };\n },\n });\n\n add({\n name: 'save_document',\n title: 'Save the document',\n description: 'Returns the document as JSON (format pixerate-paint, version 1): canvas, paper, layers with PNG data URLs, and the recorded strokes unless strokes is false. Changes nothing.',\n inputSchema: { type: 'object', properties: { strokes: { type: 'boolean', description: 'Include the recorded strokes (default true).' } }, additionalProperties: false },\n annotations: { title: 'Save the document', ...READ },\n handler: ({ strokes }) => c.toJSON({ strokes }),\n });\n\n add({\n name: 'open_document',\n title: 'Open a document',\n description: 'Opens a saved paint document (as save_document returns), replacing the painting and clearing the undo history. Returns the canvas, the layers and anything that could not be read.',\n inputSchema: { type: 'object', properties: { document: { type: 'object', description: 'The saved document JSON.' } }, required: ['document'], additionalProperties: false },\n annotations: { title: 'Open a document', ...DESTRUCTIVE },\n handler: ({ document }) => {\n const problems = c.loadJSON(document);\n const d = c.document;\n return { width: d.canvas.width, height: d.canvas.height, layers: c.listLayers(), strokes: d.strokes.length, problems };\n },\n });\n\n tools.push(...createPaintGuideTools(c));\n tools.push(...createPaintWetTools(c));\n tools.push(...createPaintThickTools(c));\n tools.push(...createPaintReplayTools(c));\n tools.push(...createPaintStickerTools(c));\n tools.push(...createPigmentTools());\n\n return { id: options.id ?? 'paint-editor', namespace: options.namespace, tools };\n}\n","/**\n * Framework-free UI logic shared by the React and Svelte paint editors, so both behave the same:\n *\n * - `PaintGesture` turns pointer events (structurally typed, no DOM import) into strokes (with\n * coalesced samples and a predicted tail), colour picks, pans, view rotation and zoom, with palm\n * rejection while a pen is down.\n * - `PaintCanvasPresenter` keeps a document-sized 2D canvas up to date from the compositor,\n * uploading only the tiles that changed.\n * - Guide overlays as line segments, and the slider tables of the brush and paper panels.\n */\n\nimport type { MixerLoad } from '../paint/exchange';\nimport { BLEND_MODES, type BlendModeInfo } from '../raster/blend';\nimport type { ImpastoTool, PaintBrush, PaintBrushInput, WetMediumKind, WetTool } from './brushes';\nimport { TILE, type RasterRect } from '../raster/surface';\nimport type { PaintEditorController } from './controller';\nimport type { PenPoint } from './engine';\nimport { perspectiveGuideLines, symmetryAxisSegments, type Point2 } from './guides';\nimport { stencilOutline, type StencilInput } from './stencils';\nimport type { ParticleKind, ParticleRender } from './procedural';\nimport type { StampKind } from './stickers';\nimport type { WetEngineInfo, WetGpuMode } from './wet';\nimport { isPalmTouch, pointerToPenPoint, type PointerSample } from './input';\nimport { MAX_ZOOM, MIN_ZOOM, screenToDocument } from './view';\nimport { hsvToRgb, parseHexColor, rgbToHexColor, rgbToHsv, wheelToHue, type ColorWheel } from './color';\n\n/** The Pointer Event fields the gesture reads. */\nexport interface PaintPointerEvent extends PointerSample {\n pointerId: number;\n clientX: number;\n clientY: number;\n button?: number;\n buttons?: number;\n shiftKey?: boolean;\n altKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n getCoalescedEvents?(): PaintPointerEvent[];\n getPredictedEvents?(): PaintPointerEvent[];\n}\n\nexport type PaintTool = 'brush' | 'eyedropper' | 'pan' | 'rotate';\n\nexport const PAINT_TOOLS: ReadonlyArray<{ id: PaintTool; label: string; key: string }> = [\n { id: 'brush', label: 'Brush', key: 'b' },\n { id: 'eyedropper', label: 'Eyedropper', key: 'i' },\n { id: 'pan', label: 'Pan', key: 'h' },\n { id: 'rotate', label: 'Rotate view', key: 'r' },\n];\n\n/** Where the viewport element is on screen and how big it is (CSS pixels). */\nexport interface ViewportBox {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\ntype Mode = { kind: 'stroke'; pointerId: number } | { kind: 'pan'; pointerId: number; x: number; y: number; pan: Point2 } | { kind: 'rotate'; pointerId: number; angle: number; rotation: number } | null;\n\nexport class PaintGesture {\n tool: PaintTool = 'brush';\n /** The last refusal (a hidden or locked layer), for the editor to show. */\n error: string | null = null;\n /** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */\n predicted: PenPoint[] = [];\n private mode: Mode = null;\n private penAt: Point2 | null = null;\n private penDown = false;\n\n constructor(\n private readonly controller: PaintEditorController,\n private readonly box: () => ViewportBox\n ) {}\n\n private doc(e: { clientX: number; clientY: number }): Point2 {\n const b = this.box();\n const s = this.controller.getState();\n return screenToDocument(s.view, s.document.canvas, b, e.clientX - b.left, e.clientY - b.top);\n }\n\n private pen(e: PaintPointerEvent): PenPoint {\n const p = this.doc(e);\n return pointerToPenPoint(e, p.x, p.y, this.controller.getState().view);\n }\n\n /** Pointer down; returns true when the gesture took the pointer (capture it). */\n down(e: PaintPointerEvent): boolean {\n this.error = null;\n if (this.mode) return false;\n if (e.pointerType === 'pen') {\n this.penDown = true;\n this.penAt = { x: e.clientX, y: e.clientY };\n } else if (e.pointerType === 'touch' && (this.penDown || isPalmTouch({ x: e.clientX, y: e.clientY }, this.penAt, this.controller.getState().hand))) {\n return false;\n }\n const middle = e.button === 1;\n const tool = middle ? 'pan' : this.tool;\n if (tool === 'pan') {\n this.mode = { kind: 'pan', pointerId: e.pointerId, x: e.clientX, y: e.clientY, pan: { ...this.controller.getState().view.pan } };\n return true;\n }\n if (tool === 'rotate') {\n const b = this.box();\n this.mode = { kind: 'rotate', pointerId: e.pointerId, angle: Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2), rotation: this.controller.getState().view.rotation };\n return true;\n }\n if (tool === 'eyedropper' || e.altKey) {\n this.pick(e);\n return false;\n }\n try {\n const p = this.pen(e);\n const session = this.controller.beginStroke({ start: p });\n session.push([p]);\n this.mode = { kind: 'stroke', pointerId: e.pointerId };\n return true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n return false;\n }\n }\n\n private pick(e: PaintPointerEvent): void {\n const p = this.doc(e);\n const { width, height } = this.controller.document.canvas;\n if (p.x < 0 || p.y < 0 || p.x >= width || p.y >= height) return;\n this.controller.setColor(this.controller.pickColor(p.x, p.y).color);\n }\n\n move(e: PaintPointerEvent): void {\n if (e.pointerType === 'pen') this.penAt = { x: e.clientX, y: e.clientY };\n const m = this.mode;\n if (!m || m.pointerId !== e.pointerId) return;\n if (m.kind === 'pan') {\n this.controller.setView({ pan: { x: m.pan.x + e.clientX - m.x, y: m.pan.y + e.clientY - m.y } });\n return;\n }\n if (m.kind === 'rotate') {\n const b = this.box();\n const a = Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2);\n let r = m.rotation + ((a - m.angle) * 180) / Math.PI;\n if (e.shiftKey) r = Math.round(r / 15) * 15;\n this.controller.setView({ rotation: r });\n return;\n }\n const session = this.controller.activeStroke;\n if (!session) return;\n const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];\n session.push((events.length ? events : [e]).map((ev) => this.pen(ev)));\n const predicted = typeof e.getPredictedEvents === 'function' ? e.getPredictedEvents() : [];\n this.predicted = predicted.map((ev) => this.pen(ev));\n }\n\n up(e: PaintPointerEvent): void {\n if (e.pointerType === 'pen') this.penDown = false;\n const m = this.mode;\n if (!m || m.pointerId !== e.pointerId) return;\n this.mode = null;\n this.predicted = [];\n if (m.kind === 'stroke') this.controller.activeStroke?.end();\n }\n\n cancel(): void {\n this.penDown = false;\n this.predicted = [];\n if (this.mode?.kind === 'stroke') this.controller.activeStroke?.cancel();\n this.mode = null;\n }\n\n get active(): boolean {\n return this.mode !== null;\n }\n\n /** Wheel: zoom about the pointer (Ctrl / pinch or plain wheel), Shift+wheel rotates the view. */\n wheel(e: { clientX: number; clientY: number; deltaY: number; shiftKey?: boolean }): void {\n const s = this.controller.getState();\n if (e.shiftKey) {\n this.controller.setView({ rotation: s.view.rotation + (e.deltaY > 0 ? 5 : -5) });\n return;\n }\n const b = this.box();\n const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, s.view.zoom * Math.exp(-e.deltaY * 0.0015)));\n const k = zoom / s.view.zoom;\n // Keep the point under the pointer still: the pan scales about it.\n const px = e.clientX - b.left - b.width / 2;\n const py = e.clientY - b.top - b.height / 2;\n this.controller.setView({ zoom, pan: { x: px - (px - s.view.pan.x) * k, y: py - (py - s.view.pan.y) * k } });\n }\n}\n\n/** Keyboard shortcuts shared by both editors; returns true when it handled the key. */\nexport function handlePaintShortcut(\n controller: PaintEditorController,\n gesture: PaintGesture,\n e: { key: string; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; target?: unknown }\n): boolean {\n const mod = e.ctrlKey || e.metaKey;\n const key = e.key.toLowerCase();\n if (mod && key === 'z') {\n if (controller.getState().stroking) return true;\n if (e.shiftKey) controller.redo();\n else controller.undo();\n return true;\n }\n if (mod && key === 'y') {\n controller.redo();\n return true;\n }\n if (mod) return false;\n const tool = PAINT_TOOLS.find((t) => t.key === key);\n if (tool) {\n gesture.tool = tool.id;\n return true;\n }\n const brush = controller.getState().brush;\n if (key === '[' || key === ']') {\n const step = Math.max(1, Math.round(brush.tip.size * 0.15));\n controller.setBrush({ tip: { size: Math.max(1, brush.tip.size + (key === ']' ? step : -step)) } });\n return true;\n }\n if (key === 'x') {\n controller.swapColors();\n return true;\n }\n if (key === 'm') {\n controller.setView({ mirror: !controller.getState().view.mirror });\n return true;\n }\n if (key === 'escape') {\n controller.resetView();\n return true;\n }\n return false;\n}\n\n/** The 2D context methods the presenter uses (a `CanvasRenderingContext2D`). */\nexport interface Paint2DContext {\n createImageData(width: number, height: number): { data: Uint8ClampedArray };\n putImageData(image: never, dx: number, dy: number, sx?: number, sy?: number, sw?: number, sh?: number): void;\n}\n\n/** Keeps a document-sized canvas in step with the controller, tile by tile. */\nexport class PaintCanvasPresenter {\n private readonly uploaded = new Map<string, Uint8ClampedArray>();\n private scratch: { data: Uint8ClampedArray } | null = null;\n private size = '';\n\n constructor(private readonly controller: PaintEditorController) {}\n\n /** Draws what changed into `ctx` (a canvas the size of the document); returns the tiles drawn. */\n render(ctx: Paint2DContext): number {\n const { width, height } = this.controller.document.canvas;\n const size = `${width}x${height}`;\n let dirty = this.controller.takeDirty();\n if (size !== this.size) {\n this.size = size;\n this.uploaded.clear();\n dirty = 'all';\n }\n let drawn = 0;\n const rect: RasterRect = { x: 0, y: 0, width, height };\n this.controller.compositeTiles(rect, (tx, ty, data) => {\n const key = `${tx},${ty}`;\n if (this.uploaded.get(key) === data) return;\n this.uploaded.set(key, data);\n if (!this.scratch) this.scratch = ctx.createImageData(TILE, TILE);\n this.scratch.data.set(data);\n const ox = tx * TILE;\n const oy = ty * TILE;\n ctx.putImageData(this.scratch as never, ox, oy, 0, 0, Math.min(TILE, width - ox), Math.min(TILE, height - oy));\n drawn++;\n }, { dirty });\n return drawn;\n }\n}\n\n/** Lines to draw over the canvas for the guides, in document pixels, with a style key. */\nexport function guideOverlay(controller: PaintEditorController): Array<{ from: Point2; to: Point2; kind: 'symmetry' | 'perspective' | 'snap' | 'stencil' }> {\n const s = controller.getState();\n const size = s.document.canvas;\n const out: Array<{ from: Point2; to: Point2; kind: 'symmetry' | 'perspective' | 'snap' | 'stencil' }> = [];\n for (const st of s.guides.stencils) {\n if (!st.enabled) continue;\n const line = stencilOutline(st);\n for (let i = 0; i + 1 < line.length; i++) out.push({ from: line[i], to: line[i + 1], kind: 'stencil' });\n }\n for (const [from, to] of symmetryAxisSegments(s.guides.symmetry, size)) out.push({ from, to, kind: 'symmetry' });\n if (s.guides.perspective.enabled) for (const [from, to] of perspectiveGuideLines(s.guides.perspective, size, 16)) out.push({ from, to, kind: 'perspective' });\n return out;\n}\n\n/** One slider of a panel: a path into the brush (or paper) settings. */\nexport interface SliderControl {\n path: string;\n label: string;\n min: number;\n max: number;\n step: number;\n}\n\nexport const BRUSH_SLIDERS: readonly SliderControl[] = [\n { path: 'tip.size', label: 'Size', min: 1, max: 400, step: 1 },\n { path: 'tip.opacity', label: 'Opacity', min: 0, max: 1, step: 0.01 },\n { path: 'tip.flow', label: 'Flow', min: 0, max: 1, step: 0.01 },\n { path: 'tip.hardness', label: 'Hardness', min: 0, max: 1, step: 0.01 },\n { path: 'tip.spacing', label: 'Spacing', min: 0.01, max: 1, step: 0.01 },\n { path: 'tip.roundness', label: 'Roundness', min: 0.05, max: 1, step: 0.01 },\n { path: 'tip.angle', label: 'Angle', min: -180, max: 180, step: 1 },\n { path: 'grain', label: 'Grain', min: 0, max: 1, step: 0.01 },\n { path: 'tip.smoothing', label: 'Smoothing', min: 0, max: 0.95, step: 0.01 },\n { path: 'pressureSmoothing', label: 'Pressure smoothing', min: 0, max: 0.95, step: 0.01 },\n { path: 'dynamics.minSize', label: 'Min size', min: 0, max: 1, step: 0.01 },\n { path: 'dynamics.tiltSize', label: 'Tilt size', min: 0, max: 1, step: 0.01 },\n { path: 'dynamics.velocitySize', label: 'Speed size', min: -1, max: 1, step: 0.01 },\n];\n\nexport const PAPER_SLIDERS: readonly SliderControl[] = [\n { path: 'depth', label: 'Grain depth', min: 0, max: 1, step: 0.01 },\n { path: 'scale', label: 'Grain scale', min: 0.25, max: 8, step: 0.05 },\n { path: 'shading', label: 'Shading', min: 0, max: 1, step: 0.01 },\n { path: 'dry.contrast', label: 'Contrast', min: 0, max: 3, step: 0.05 },\n { path: 'absorption', label: 'Absorption', min: 0, max: 1, step: 0.01 },\n { path: 'permeability', label: 'Permeability', min: 0, max: 1, step: 0.01 },\n { path: 'roughness', label: 'Roughness', min: 0, max: 1, step: 0.01 },\n { path: 'metallic', label: 'Metallic', min: 0, max: 1, step: 0.01 },\n];\n\n/** Reads `a.b` from an object. */\nexport function readPath(obj: unknown, path: string): number {\n let v: unknown = obj;\n for (const k of path.split('.')) v = (v as Record<string, unknown>)?.[k];\n return typeof v === 'number' ? v : 0;\n}\n\n/** `{a: {b: value}}` for `a.b`, keeping the rest of `obj`'s `a`. */\nexport function patchPath(obj: Record<string, unknown>, path: string, value: number): Record<string, unknown> {\n const [head, ...rest] = path.split('.');\n if (!rest.length) return { [head]: value };\n return { [head]: { ...(obj[head] as Record<string, unknown>), ...patchPath((obj[head] as Record<string, unknown>) ?? {}, rest.join('.'), value) } };\n}\n\n/** The point on a colour wheel of `radius` (centre 0, 0) for a wheel angle in degrees, red at the top. */\nexport function wheelPoint(angle: number, radius: number): Point2 {\n const a = ((angle - 90) * Math.PI) / 180;\n return { x: Math.cos(a) * radius, y: Math.sin(a) * radius };\n}\n\n/** The wheel angle (degrees, red at the top, clockwise) of a point relative to the wheel centre. */\nexport function wheelAngle(x: number, y: number): number {\n return ((Math.atan2(y, x) * 180) / Math.PI + 90 + 360) % 360;\n}\n\n/**\n * A hue ring for a colour wheel, `size × size` RGBA: hues at full saturation and value between\n * `inner` and 1 of the radius, red at the top, laid out on the RYB or RGB wheel.\n */\nexport function renderColorWheel(size: number, wheel: ColorWheel, inner = 0.64): { width: number; height: number; data: Uint8ClampedArray } {\n const data = new Uint8ClampedArray(size * size * 4);\n const r = size / 2;\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n const dx = x + 0.5 - r;\n const dy = y + 0.5 - r;\n const d = Math.hypot(dx, dy) / r;\n if (d > 1 || d < inner) continue;\n const [cr, cg, cb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), 1, 1);\n const i = (y * size + x) * 4;\n // Anti-aliased edges.\n const edge = Math.min(1, (1 - d) * r, (d - inner) * r);\n data[i] = cr;\n data[i + 1] = cg;\n data[i + 2] = cb;\n data[i + 3] = Math.round(255 * Math.max(0, edge));\n }\n }\n return { width: size, height: size, data };\n}\n\n/** The colour for a click on the wheel ring at (dx, dy) from its centre, keeping `color`'s saturation and value. */\nexport function wheelPick(dx: number, dy: number, wheel: ColorWheel, color: string): string {\n const [r, g, b] = parseHexColor(color);\n const { s, v } = rgbToHsv(r, g, b);\n const [nr, ng, nb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), s || 1, v || 1);\n return rgbToHexColor(nr, ng, nb);\n}\n\n/** A group of the blend menu: the Photoshop modes, then pigment and the painterly modes. */\nexport interface BlendMenuGroup {\n label: string;\n modes: readonly BlendModeInfo[];\n}\n\n/**\n * The layer and brush blend menus as groups: the 26 standard modes, then the paint modes\n * (`pigment` and the painterly `watercolor`, `tint`, `highlight`, `shadow`), each with its\n * description as a tooltip. Both bindings render this as `<optgroup>`s.\n */\nexport const BLEND_MENU_GROUPS: readonly BlendMenuGroup[] = [\n { label: 'Standard', modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== 'pigment') },\n { label: 'Paint', modes: BLEND_MODES.filter((m) => m.painterly || m.id === 'pigment') },\n];\n\n/** The engine-specific part of the brush panel: which sliders and toggles the active brush has. */\nexport interface BrushEngineControls {\n /** Section heading. */\n title: string;\n sliders: readonly SliderControl[];\n /** Where the RGB / pigment choice is stored, or `null` when the engine has none. */\n mixingPath: 'behavior.mixing' | 'blender.mixing' | 'smudge.mixing' | null;\n /** The dirty-brush toggle (`behavior.dirty`). */\n dirty: boolean;\n /** The multi-colour controls (`behavior.multicolor`; the panels show the brush gradient, `brush.paint`, instead). */\n multicolor: boolean;\n /** Load / Clean buttons and a swatch of what the brush carries. */\n load: boolean;\n /** The wet tool menu (`wet.tool`, `WET_TOOLS`) and medium menu (`wet.medium`, `WET_MEDIA`). */\n wetTool?: boolean;\n /** The impasto tool menu (`impasto.tool`, `IMPASTO_TOOLS`). */\n impastoTool?: boolean;\n /** The particle kind and render menus (`particle.kind`, `particle.render`). */\n particleMenus?: boolean;\n /** The sticker stamp toggles (`sticker.stamps`, `STAMP_KINDS`). */\n stickerStamps?: boolean;\n}\n\nconst PICKUP_SLIDERS: readonly SliderControl[] = [\n { path: 'behavior.pickup', label: 'Pick-up', min: 0, max: 1, step: 0.01 },\n { path: 'behavior.mix', label: 'Mix', min: 0, max: 1, step: 0.01 },\n];\n\n/**\n * The controls the active brush's engine adds to the brush panel (`null` for the eraser):\n * blender strength and blend distance; smudge strength and length; the Mixer Brush's pick-up,\n * mix and load; a dab brush's pick-up and mix ratios. Every engine but the eraser has an RGB /\n * pigment choice; dab and mixer brushes also have the dirty and multi-colour controls. Pick-up\n * and mix always go through `behavior`, which keeps a Mixer Brush's `mixer` block in sync.\n */\nexport function brushEngineControls(brush: Pick<PaintBrush, 'engine'> & { wet?: PaintBrush['wet'] }): BrushEngineControls | null {\n switch (brush.engine) {\n case 'particle':\n return {\n title: 'Particles',\n sliders: [\n { path: 'particle.count', label: 'Particles', min: 1, max: 64, step: 1 },\n { path: 'particle.size', label: 'Mark size', min: 0, max: 1, step: 0.01 },\n { path: 'particle.spread', label: 'Spread', min: 0, max: 1, step: 0.01 },\n { path: 'particle.stiffness', label: 'Stiffness', min: 0, max: 1, step: 0.01 },\n { path: 'particle.damping', label: 'Damping', min: 0, max: 1, step: 0.01 },\n { path: 'particle.speed', label: 'Speed', min: 0, max: 1, step: 0.01 },\n { path: 'particle.gravity', label: 'Gravity', min: -1, max: 1, step: 0.01 },\n { path: 'particle.life', label: 'Life', min: 0, max: 1, step: 0.01 },\n { path: 'particle.interval', label: 'Bead spacing', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: null,\n dirty: false,\n multicolor: false,\n load: false,\n particleMenus: true,\n };\n case 'bristle':\n return {\n title: 'Bristles',\n sliders: [\n { path: 'bristle.count', label: 'Bristles', min: 3, max: 128, step: 1 },\n { path: 'bristle.stiffness', label: 'Stiffness', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.splay', label: 'Splay', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.thickness', label: 'Bristle width', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.load', label: 'Load', min: 0, max: 1, step: 0.01 },\n { path: 'bristle.jitter', label: 'Variation', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: null,\n dirty: false,\n multicolor: false,\n load: false,\n };\n case 'sticker':\n return {\n title: 'Stickers',\n sliders: [\n { path: 'sticker.density', label: 'Density', min: 0, max: 1, step: 0.01 },\n { path: 'sticker.sizeJitter', label: 'Size jitter', min: 0, max: 1, step: 0.01 },\n { path: 'sticker.rotationJitter', label: 'Turn jitter', min: 0, max: 1, step: 0.01 },\n { path: 'sticker.scatter', label: 'Scatter', min: 0, max: 2, step: 0.01 },\n { path: 'sticker.colorJitter', label: 'Colour jitter', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: null,\n dirty: false,\n multicolor: false,\n load: false,\n stickerStamps: true,\n };\n case 'impasto':\n return {\n title: 'Thick paint',\n sliders: [\n { path: 'impasto.thickness', label: 'Thickness', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.gloss', label: 'Gloss', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.metallic', label: 'Metallic', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.load', label: 'Load', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.thinner', label: 'Thinner', min: 0, max: 1, step: 0.01 },\n { path: 'impasto.drag', label: 'Drag', min: 0, max: 1, step: 0.01 },\n ...PICKUP_SLIDERS,\n ],\n mixingPath: 'behavior.mixing',\n dirty: true,\n multicolor: false,\n load: true,\n impastoTool: true,\n };\n case 'wet': {\n const tool = brush.wet?.tool ?? 'paint';\n const sliders: SliderControl[] =\n tool === 'paint'\n ? [\n { path: 'wet.water', label: 'Water', min: 0, max: 1, step: 0.01 },\n { path: 'wet.pigment', label: 'Pigment', min: 0, max: 1, step: 0.01 },\n { path: 'wet.body', label: 'Body (opacity)', min: 0, max: 1, step: 0.01 },\n { path: 'wet.rewet', label: 'Re-wet', min: 0, max: 1, step: 0.01 },\n { path: 'wet.load', label: 'Load', min: 0, max: 1, step: 0.01 },\n { path: 'wet.thinner', label: 'Thinner', min: 0, max: 1, step: 0.01 },\n ...PICKUP_SLIDERS,\n ]\n : tool === 'water'\n ? [\n { path: 'wet.water', label: 'Water', min: 0, max: 1, step: 0.01 },\n { path: 'wet.rewet', label: 'Re-wet', min: 0, max: 1, step: 0.01 },\n ]\n : [{ path: 'wet.strength', label: 'Strength', min: 0, max: 1, step: 0.01 }];\n return { title: 'Wet brush', sliders, mixingPath: tool === 'paint' ? 'behavior.mixing' : null, dirty: tool === 'paint', multicolor: false, load: tool === 'paint', wetTool: true };\n }\n case 'blender':\n return {\n title: 'Blender',\n sliders: [\n { path: 'blender.strength', label: 'Strength', min: 0, max: 1, step: 0.01 },\n { path: 'blender.distance', label: 'Blend distance', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: 'blender.mixing',\n dirty: false,\n multicolor: false,\n load: false,\n };\n case 'smudge':\n return {\n title: 'Smudge',\n sliders: [\n { path: 'smudge.strength', label: 'Strength', min: 0, max: 1, step: 0.01 },\n { path: 'smudge.length', label: 'Length', min: 0, max: 1, step: 0.01 },\n ],\n mixingPath: 'smudge.mixing',\n dirty: false,\n multicolor: false,\n load: false,\n };\n case 'mixer':\n return { title: 'Mixer Brush', sliders: [...PICKUP_SLIDERS, { path: 'mixer.load', label: 'Load', min: 0, max: 1, step: 0.01 }], mixingPath: 'behavior.mixing', dirty: true, multicolor: true, load: true };\n case 'dab':\n return { title: 'Paint behaviour', sliders: PICKUP_SLIDERS, mixingPath: 'behavior.mixing', dirty: true, multicolor: true, load: true };\n default:\n return null;\n }\n}\n\n/** The multi-colour sliders for a mode: softness, and dabs per colour in `dab` mode. */\nexport function multiColorSliders(mode: string): readonly SliderControl[] {\n if (mode === 'off') return [];\n const soft: SliderControl = { path: 'behavior.multicolor.softness', label: 'Colour softness', min: 0, max: 1, step: 0.01 };\n return mode === 'dab' ? [soft, { path: 'behavior.multicolor.length', label: 'Dabs per colour', min: 1, max: 100, step: 1 }] : [soft];\n}\n\n/** A brush change setting `path` (e.g. `smudge.mixing`, `behavior.dirty`) to any value, keeping the rest of its block. */\nexport function patchBrushValue(brush: PaintBrush, path: string, value: unknown): PaintBrushInput {\n const set = (obj: Record<string, unknown>, keys: string[]): Record<string, unknown> => {\n const [head, ...rest] = keys;\n if (!rest.length) return { ...obj, [head]: value };\n return { ...obj, [head]: set((obj[head] as Record<string, unknown>) ?? {}, rest) };\n };\n const [head, ...rest] = path.split('.');\n if (!rest.length) return { [head]: value } as PaintBrushInput;\n return { [head]: set({ ...((brush as unknown as Record<string, Record<string, unknown>>)[head] ?? {}) }, rest) } as PaintBrushInput;\n}\n\n/** What the brush carries (picked-up paint, else loaded paint) as #rrggbb, or `null` when it is clean and empty. */\nexport function brushLoadColor(load: MixerLoad): string | null {\n const c = load.pickup ?? load.reservoir;\n return c ? rgbToHexColor(Math.round(c[0] * 255), Math.round(c[1] * 255), Math.round(c[2] * 255)) : null;\n}\n\n/** Sample sizes (square, in pad pixels) for picking from the mixing pad. */\nexport const PAD_SAMPLE_SIZES: readonly number[] = [1, 3, 5, 9, 15];\n\n/** The averaging radius `mixingPadPick` takes for a sample size: 1 → 0, 3 → 1, 5 → 2 … */\nexport function padSampleRadius(size: number): number {\n return Math.max(0, Math.floor((size - 1) / 2));\n}\n\n// ---------------------------------------------------------------------------\n// Wet media\n// ---------------------------------------------------------------------------\n\n/** Labels of the wet tools, for the wet brush's tool menu. */\nexport const WET_TOOL_LABELS: Readonly<Record<WetTool, string>> = {\n paint: 'Paint',\n water: 'Add water',\n dry: 'Dry',\n blow: 'Blow',\n blend: 'Blend',\n smudge: 'Smudge',\n blot: 'Blot (erase wet)',\n};\n\n/** The wet panel's sliders for a layer's wet settings (paths into `WetParams`). */\nexport const WET_PARAM_SLIDERS: readonly SliderControl[] = [\n { path: 'viscosity', label: 'Viscosity', min: 0, max: 1, step: 0.01 },\n { path: 'evaporation', label: 'Evaporation', min: 0, max: 1, step: 0.01 },\n { path: 'edgeDarkening', label: 'Edge darkening', min: 0, max: 1, step: 0.01 },\n { path: 'granulation', label: 'Granulation', min: 0, max: 1, step: 0.01 },\n { path: 'bloom', label: 'Blooms', min: 0, max: 1, step: 0.01 },\n { path: 'bleed', label: 'Colour bleed', min: 0, max: 1, step: 0.01 },\n { path: 'pigmentWeight', label: 'Pigment weight', min: 0, max: 1, step: 0.01 },\n { path: 'pickup', label: 'Lifting', min: 0, max: 1, step: 0.01 },\n { path: 'fixture', label: 'Staining', min: 0, max: 1, step: 0.01 },\n];\n\n/** The canvas tilt sliders. */\nexport const WET_TILT_SLIDERS: readonly SliderControl[] = [\n { path: 'angle', label: 'Tilt', min: 0, max: 90, step: 1 },\n { path: 'direction', label: 'Downhill', min: 0, max: 359, step: 1 },\n];\n\n/** The speed slider (steps per second). */\nexport const WET_SPEED_SLIDER: SliderControl = { path: 'speed', label: 'Speed', min: 15, max: 480, step: 15 };\n\n/** The wet-engine choices a panel offers (Engine B, the WebGPU accelerator). */\nexport const WET_GPU_OPTIONS: readonly { value: WetGpuMode; label: string }[] = [\n { value: 'auto', label: 'GPU for large washes' },\n { value: 'off', label: 'CPU only' },\n];\n\n/** One line saying what runs the wet paint (for panels). */\nexport function wetEngineSummary(e: WetEngineInfo): string {\n const gpu = e.adapter ? `GPU (${e.adapter})` : 'GPU';\n switch (e.state) {\n case 'off':\n return 'CPU only (GPU off).';\n case 'not-started':\n return 'The GPU starts with the first wet stroke.';\n case 'starting':\n return 'Starting the GPU…';\n case 'active':\n return `${gpu} shading ${e.activeTiles} wet tiles; the CPU keeps the result.`;\n case 'standby':\n return `${gpu} ready; it takes over at ${e.minTiles} wet tiles (${e.activeTiles} now).`;\n default:\n return e.reason ?? 'The GPU is not in use.';\n }\n}\n\n/**\n * Runs the wet paint's clock with the display: `controller.tickWet(now)` every animation frame\n * (it does nothing while nothing is wet or time is paused). Returns a function that stops it.\n * Bindings call it once per mounted canvas.\n */\nexport function startWetClock(controller: PaintEditorController, frame?: (cb: (t: number) => void) => number, cancel?: (id: number) => void): () => void {\n const g = globalThis as { requestAnimationFrame?: (cb: (t: number) => void) => number; cancelAnimationFrame?: (id: number) => void };\n const raf = frame ?? g.requestAnimationFrame;\n const caf = cancel ?? g.cancelAnimationFrame;\n if (!raf) return () => {};\n let stopped = false;\n let id = 0;\n const loop = (t: number) => {\n if (stopped) return;\n controller.tickWet(t).catch(() => {});\n id = raf(loop);\n };\n id = raf(loop);\n return () => {\n stopped = true;\n caf?.(id);\n };\n}\n\n/**\n * Draws the water overlay (blue where the surface is wet, faint where the paper is damp) into a\n * document-sized 2D context, clearing it first. Draws nothing when the overlay is off.\n */\nexport function drawWaterOverlay(controller: PaintEditorController, ctx: Paint2DContext & { clearRect(x: number, y: number, w: number, h: number): void }): number {\n const { width, height } = controller.document.canvas;\n ctx.clearRect(0, 0, width, height);\n if (!controller.getState().wet.overlay) return 0;\n const tiles = controller.waterOverlayTiles({ x: 0, y: 0, width, height });\n if (!tiles.length) return 0;\n const img = ctx.createImageData(TILE, TILE);\n for (const t of tiles) {\n img.data.set(t.data);\n ctx.putImageData(img as never, t.tx * TILE, t.ty * TILE, 0, 0, Math.min(TILE, width - t.tx * TILE), Math.min(TILE, height - t.ty * TILE));\n }\n return tiles.length;\n}\n\n/** Labels of the wet media, for the wet brush's medium menu. */\nexport const WET_MEDIUM_LABELS: Readonly<Record<WetMediumKind, string>> = { watercolor: 'Watercolour', gouache: 'Gouache', oil: 'Oil' };\n\n/** Labels of the impasto tools. */\nexport const IMPASTO_TOOL_LABELS: Readonly<Record<ImpastoTool, string>> = { brush: 'Brush', tube: 'Paint tube', knife: 'Palette knife', roller: 'Roller', gloop: 'Gloop pen' };\n\n/** The light sliders (thick paint and metallic paper). */\nexport const LIGHT_SLIDERS: readonly SliderControl[] = [\n { path: 'azimuth', label: 'Light direction', min: 0, max: 359, step: 1 },\n { path: 'elevation', label: 'Light height', min: 5, max: 90, step: 1 },\n { path: 'relief', label: 'Relief', min: 0, max: 1, step: 0.01 },\n];\n\n/** Labels of the particle kinds and renders. */\nexport const PARTICLE_KIND_LABELS: Readonly<Record<ParticleKind, string>> = { string: 'Strings', spring: 'Springs', orbit: 'Orbits', flow: 'Flow' };\nexport const PARTICLE_RENDER_LABELS: Readonly<Record<ParticleRender, string>> = { dot: 'Dots', line: 'Lines', sequence: 'Beads' };\n\n/** Labels of the sticker stamps. */\nexport const STAMP_LABELS: Readonly<Record<StampKind, string>> = { dot: 'Dot', star: 'Star', heart: 'Heart', leaf: 'Leaf', flower: 'Flower', confetti: 'Confetti', splat: 'Splat', sparkle: 'Sparkle', ring: 'Ring' };\n\n/** Toggles one stamp in a sticker brush's set (never leaving it empty). */\nexport function toggleStamp(stamps: readonly StampKind[], stamp: StampKind): StampKind[] {\n const next = stamps.includes(stamp) ? stamps.filter((s) => s !== stamp) : [...stamps, stamp];\n return next.length ? next : [stamp];\n}\n\n/** The sticker panel's sliders for the selected sticker. */\nexport const STICKER_SLIDERS: readonly SliderControl[] = [\n { path: 'size', label: 'Size', min: 2, max: 400, step: 1 },\n { path: 'rotation', label: 'Rotation', min: -180, max: 180, step: 1 },\n { path: 'opacity', label: 'Opacity', min: 0, max: 1, step: 0.01 },\n];\n\n/** Stencils a toolbar can lay with one click, centred on the canvas. */\nexport function quickStencil(kind: 'ruler' | 'circle' | 'ellipse-mask' | 'edge-mask', canvas: { width: number; height: number }): StencilInput {\n const { width: w, height: h } = canvas;\n switch (kind) {\n case 'ruler':\n return { kind: 'ruler', from: { x: w * 0.1, y: h / 2 }, to: { x: w * 0.9, y: h / 2 } };\n case 'circle':\n return { kind: 'arc', center: { x: w / 2, y: h / 2 }, radius: Math.min(w, h) * 0.3 };\n case 'ellipse-mask':\n return { kind: 'shape', shape: 'ellipse', rect: { x: w * 0.2, y: h * 0.2, width: w * 0.6, height: h * 0.6 } };\n case 'edge-mask':\n return { kind: 'line', from: { x: w / 2, y: 0 }, to: { x: w / 2, y: h } };\n }\n}\n"]}
|