@pixerate/editor 0.102.0 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/THIRD_PARTY_NOTICES.md +1 -0
  2. package/dist/agent/index.cjs +7 -7
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.js +7 -7
  5. package/dist/canvas/index.cjs +3 -3
  6. package/dist/canvas/index.cjs.map +1 -1
  7. package/dist/canvas/index.js +4 -4
  8. package/dist/chunk-6BU5WU4R.cjs +1 -0
  9. package/dist/chunk-6BU5WU4R.cjs.map +1 -0
  10. package/dist/{chunk-IB53LVGV.js → chunk-DS7KDKPX.js} +1 -1
  11. package/dist/chunk-NQLQ2JOI.js +7176 -0
  12. package/dist/chunk-NQLQ2JOI.js.map +1 -0
  13. package/dist/{chunk-ZZOTHJ4K.cjs → chunk-PV42JITT.cjs} +1 -1
  14. package/dist/{chunk-ZZOTHJ4K.cjs.map → chunk-PV42JITT.cjs.map} +1 -1
  15. package/dist/chunk-VSMM7B3P.cjs +7176 -0
  16. package/dist/chunk-VSMM7B3P.cjs.map +1 -0
  17. package/dist/chunk-XYBJKUAU.js +1 -0
  18. package/dist/chunk-XYBJKUAU.js.map +1 -0
  19. package/dist/compositor-B1ZrKR89.d.ts +835 -0
  20. package/dist/compositor-CTMU-B1t.d.cts +835 -0
  21. package/dist/export-BLpMyCYg.d.ts +3139 -0
  22. package/dist/export-X9rY79p0.d.cts +3139 -0
  23. package/dist/homography-CZ-tFjay.d.cts +50 -0
  24. package/dist/homography-Ctfjoc1b.d.ts +50 -0
  25. package/dist/image-editor/index.cjs +2846 -9407
  26. package/dist/image-editor/index.cjs.map +1 -1
  27. package/dist/image-editor/index.d.cts +24 -3151
  28. package/dist/image-editor/index.d.ts +24 -3151
  29. package/dist/image-editor/index.js +2508 -9069
  30. package/dist/image-editor/index.js.map +1 -1
  31. package/dist/mixer-DG_9Yu2W.d.ts +282 -0
  32. package/dist/mixer-DGcH6HLu.d.cts +282 -0
  33. package/dist/paint/index.cjs +4 -3
  34. package/dist/paint/index.cjs.map +1 -1
  35. package/dist/paint/index.d.cts +3 -281
  36. package/dist/paint/index.d.ts +3 -281
  37. package/dist/paint/index.js +2 -1
  38. package/dist/paint-editor/index.cjs +3330 -0
  39. package/dist/paint-editor/index.cjs.map +1 -0
  40. package/dist/paint-editor/index.d.cts +1233 -0
  41. package/dist/paint-editor/index.d.ts +1233 -0
  42. package/dist/paint-editor/index.js +3330 -0
  43. package/dist/paint-editor/index.js.map +1 -0
  44. package/dist/raster/index.d.cts +6 -835
  45. package/dist/raster/index.d.ts +6 -835
  46. package/dist/{surface-DIA-XatS.d.cts → surface-AfHCH7BR.d.cts} +1 -1
  47. package/dist/{surface-DIA-XatS.d.ts → surface-AfHCH7BR.d.ts} +1 -1
  48. package/dist/tools/index.cjs +3 -3
  49. package/dist/tools/index.cjs.map +1 -1
  50. package/dist/tools/index.js +3 -3
  51. package/dist/vector/index.d.cts +2 -1
  52. package/dist/vector/index.d.ts +2 -1
  53. package/dist/{warp-B8rAguzs.d.ts → warp-CWHZzE3x.d.ts} +2 -49
  54. package/dist/{warp-nYs2DPrx.d.cts → warp-D5jz_tN7.d.cts} +2 -49
  55. package/package.json +6 -1
  56. package/skills/README.md +1 -0
  57. package/skills/pixerate-paint-editor/SKILL.md +162 -0
  58. /package/dist/{chunk-IB53LVGV.js.map → chunk-DS7KDKPX.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/paint-editor/index.cjs","../../src/paint-editor/noise.ts","../../src/paint-editor/paper.ts","../../src/paint-editor/document.ts","../../src/paint-editor/brushes.ts","../../src/paint-editor/engine.ts","../../src/paint-editor/stroke.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/thumbnails.ts","../../src/paint-editor/controller.ts","../../src/paint-editor/tools.ts"],"names":["unit","tuning"],"mappings":"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A;AACA,IAAA;AACM,IAAA;AACZ,EAAA;AACO,EAAA;AACT;AAE6D;AAC/C,EAAA;AACA,IAAA;AACA,IAAA;AACA,MAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,MAAA;AACK,MAAA;AACF,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACS,EAAA;AACG,IAAA;AACA,IAAA;AACH,IAAA;AACT,EAAA;AACF;AAGgB;AACH,EAAA;AACb;AAMgB;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACD,EAAA;AACA,EAAA;AACE,EAAA;AACA,IAAA;AACA,IAAA;AACF,IAAA;AACD,MAAA;AACA,MAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACC,MAAA;AACD,QAAA;AACN,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACK,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACP,QAAA;AACF,MAAA;AACM,MAAA;AACE,MAAA;AACD,QAAA;AACD,QAAA;AACA,QAAA;AACN,MAAA;AACS,MAAA;AACX,IAAA;AACF,EAAA;AACO,EAAA;AACT;AR6nCe;AACA;ASjwCF;AACA;AA8BC;AAGE;AACwB,EAAA;AAC1B,EAAA;AACL,EAAA;AACT;AASS;AACyD,EAAA;AAClD,IAAA;AACJ,IAAA;AACC,IAAA;AACF,IAAA;AACC,IAAA;AACG,IAAA;AACF,IAAA;AACT,IAAA;AACF,EAAA;AACY,EAAA;AACL,EAAA;AACT;AAGgB;AACN,EAAA;AACI,EAAA;AACd;AAGsB;AACZ,EAAA;AACI,EAAA;AACd;AAUS;AACoB,EAAA;AAChB,EAAA;AACH,IAAA;AACI,MAAA;AACJ,MAAA;AACA,MAAA;AACK,MAAA;AACA,IAAA;AACN,IAAA;AACP,EAAA;AACO,EAAA;AACT;AAES;AACI,EAAA;AACoB,EAAA;AAClB,EAAA;AACL,IAAA;AACM,IAAA;AACP,IAAA;AACN,EAAA;AACM,EAAA;AACT;AAGgB;AACF,EAAA;AACF,EAAA;AAC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AAA;AACO,EAAA;AACD,EAAA;AACC,IAAA;AACX,IAAA;AACU,IAAA;AACV,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACW,IAAA;AACb,EAAA;AACA,EAAA;AACF;AAEc;AACN,EAAA;AACO,EAAA;AACD,EAAA;AACH,IAAA;AACC,IAAA;AACD,IAAA;AACP,IAAA;AACA,IAAA;AACU,IAAA;AACZ,EAAA;AACA,EAAA;AACF;AAEM;AACI,EAAA;AACI,EAAA;AACF,EAAA;AACL,EAAA;AACI,EAAA;AACT,EAAA;AACY,EAAA;AACF,EAAA;AACH,EAAA;AACK,EAAA;AACd;AAEe;AACP,EAAA;AACI,EAAA;AACA,EAAA;AACG,EAAA;AACN,EAAA;AACC,IAAA;AACN,IAAA;AACO,MAAA;AACA,MAAA;AACL,MAAA;AACK,MAAA;AACI,MAAA;AACA,MAAA;AACT,MAAA;AACS,MAAA;AACA,MAAA;AACX,IAAA;AACW,IAAA;AACX,IAAA;AACF,EAAA;AACF;AAES;AACM,EAAA;AACJ,EAAA;AACX;AAES;AACM,EAAA;AACf;AAGgB;AACJ,EAAA;AACJ,EAAA;AACO,EAAA;AAIT,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AAEA,IAAA;AACA,IAAA;AACS,IAAA;AACD,MAAA;AACA,MAAA;AACC,MAAA;AACE,QAAA;AACP,QAAA;AACO,QAAA;AACA,QAAA;AACP,QAAA;AACA,QAAA;AACA,QAAA;AACO,QAAA;AACP,QAAA;AACM,QAAA;AACA,QAAA;AACN,QAAA;AACM,QAAA;AACA,QAAA;AACN,QAAA;AACF,MAAA;AACF,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AAEA,IAAA;AACQ,MAAA;AACN,MAAA;AACS,QAAA;AACP,QAAA;AACO,QAAA;AACA,QAAA;AACT,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACU,IAAA;AACF,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,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ACA,MAAA;AACF,IAAA;AACA,IAAA;AACU,IAAA;AACJ,MAAA;AACK,MAAA;AACX,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACA,IAAA;AACU,IAAA;AACX,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AACD,MAAA;AACA,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AACD,MAAA;AACA,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAIG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACQ,MAAA;AACN,MAAA;AACE,QAAA;AACA,QAAA;AACO,QAAA;AACT,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACU,IAAA;AACJ,MAAA;AAAU,QAAA;AAAsH,MAAA;AAC9H,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACQ,MAAA;AACN,MAAA;AACE,QAAA;AACA,QAAA;AACO,QAAA;AACA,QAAA;AACT,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACS,IAAA;AACD,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AAEA,IAAA;AACA,IAAA;AACS,IAAA;AACD,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACA,IAAA;AACU,IAAA;AACX,EAAA;AAEG,EAAA;AACI,IAAA;AACC,IAAA;AACP,IAAA;AACA,IAAA;AACA,IAAA;AACU,IAAA;AACF,MAAA;AACA,MAAA;AACG,MAAA;AACX,IAAA;AACD,EAAA;AAEU,EAAA;AAEE,EAAA;AACf;Af60Ee;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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,"// Portions ported from photocraft/crates/paint/src/procedural.rs and rng.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Seeded, deterministic noise for paper grain and brush jitter (photocraft's counter-based\n * randomness and tileable value noise, on 32-bit integer hashes instead of 64-bit ones so it runs\n * fast in JavaScript). Every value is a pure function of its inputs, so a paper or a stroke\n * regenerates identically on every machine.\n */\n\n/** A 32-bit finaliser (murmur3's `fmix32`). */\nexport function mix32(h: number): number {\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return h >>> 0;\n}\n\n/** Hash of integer coordinates and a salt to [0, 1). */\nexport function hash2(x: number, y: number, salt: number): number {\n const h = mix32((Math.imul(x | 0, 0x27d4eb2d) ^ mix32((y | 0) + Math.imul(salt | 0, 0x165667b1))) >>> 0);\n return h / 4294967296;\n}\n\n/** Uniform in [0, 1) from a seed, an index and a stream (one stream per random quantity). */\nexport function rand01(seed: number, index: number, stream: number): number {\n return hash2(index, stream, seed);\n}\n\n/** Uniform in [-1, 1). */\nexport function randSigned(seed: number, index: number, stream: number): number {\n return rand01(seed, index, stream) * 2 - 1;\n}\n\n/** A seed from a string (FNV-1a, then mixed). */\nexport function seedFromString(text: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < text.length; i++) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return mix32(h >>> 0);\n}\n\nconst smooth = (t: number) => t * t * (3 - 2 * t);\n\n/** A periodic lattice of `px × py` random values, sampled with smooth interpolation. */\nclass Lattice {\n readonly values: Float32Array;\n constructor(readonly px: number, readonly py: number, salt: number) {\n this.values = new Float32Array(px * py);\n for (let j = 0; j < py; j++) for (let i = 0; i < px; i++) this.values[j * px + i] = hash2(i, j, salt);\n }\n\n /** Value noise at (x, y) in lattice cells; wraps every `px` / `py` cells. */\n at(x: number, y: number): number {\n const { px, py } = this;\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const tx = smooth(x - x0);\n const ty = smooth(y - y0);\n const i0 = ((x0 % px) + px) % px;\n const j0 = ((y0 % py) + py) % py;\n const i1 = (i0 + 1) % px;\n const j1 = (j0 + 1) % py;\n const v = this.values;\n const a = v[j0 * px + i0] + (v[j0 * px + i1] - v[j0 * px + i0]) * tx;\n const b = v[j1 * px + i0] + (v[j1 * px + i1] - v[j1 * px + i0]) * tx;\n return a + (b - a) * ty;\n }\n}\n\n/**\n * Tileable fractal value noise in 0..1 over a `size`-pixel square: `cellsX × cellsY` lattice cells\n * at the base octave (unequal counts give fibres and streaks), doubling per octave, so the result\n * repeats exactly every `size` pixels.\n */\nexport class TileableNoise {\n private readonly octaves: Lattice[] = [];\n\n constructor(readonly size: number, cellsX: number, cellsY: number, octaves: number, salt: number) {\n let cx = Math.max(1, Math.round(cellsX));\n let cy = Math.max(1, Math.round(cellsY));\n for (let o = 0; o < octaves; o++) {\n this.octaves.push(new Lattice(cx, cy, (salt + o * 7919) | 0));\n cx *= 2;\n cy *= 2;\n }\n }\n\n /** Fractal noise at pixel (x, y). */\n at(x: number, y: number): number {\n let sum = 0;\n let amp = 1;\n let norm = 0;\n const u = x / this.size;\n const v = y / this.size;\n for (const l of this.octaves) {\n sum += l.at(u * l.px, v * l.py) * amp;\n norm += amp;\n amp *= 0.5;\n }\n return norm > 0 ? sum / norm : 0.5;\n }\n}\n","/**\n * Paper: a generated grain height map (0 = valley, 1 = peak) that tiles across the canvas, a paper\n * colour, grain depth, and the three properties wet media will read later (absorption,\n * permeability, roughness). Dry media catch the peaks: a light stroke only marks the high grain, a\n * firm one fills the valleys too. Everything is generated from a pattern and a seed, so a paper is\n * a few numbers in the document, never a bitmap, and no third-party texture is bundled.\n */\n\nimport { hash2, TileableNoise } from './noise';\n\n/** Grain generators. Each is our own procedural pattern (no scanned textures). */\nexport type GrainPattern =\n | 'smooth'\n | 'cold-pressed'\n | 'hot-pressed'\n | 'rough'\n | 'vellum'\n | 'laid'\n | 'ingres'\n | 'sanded'\n | 'canvas'\n | 'linen'\n | 'gesso';\n\nexport const GRAIN_PATTERNS: readonly GrainPattern[] = ['smooth', 'cold-pressed', 'hot-pressed', 'rough', 'vellum', 'laid', 'ingres', 'sanded', 'canvas', 'linen', 'gesso'];\n\n/** Paper families, as a library groups them. */\nexport type PaperFamily = 'watercolor' | 'drawing' | 'pastel' | 'canvas' | 'panel' | 'smooth';\n\nexport const PAPER_FAMILIES: readonly PaperFamily[] = ['watercolor', 'drawing', 'pastel', 'canvas', 'panel', 'smooth'];\n\n/** How a height map is tuned for one kind of media. */\nexport interface GrainTuning {\n /** Height contrast around the middle, 0..3 (default 1). */\n contrast: number;\n /** Swap peaks and valleys (default false). */\n invert: boolean;\n}\n\nexport type PaperTiling = 'repeat' | 'mirror';\n\nexport interface PaperSettings {\n /** The library preset this paper started from (`custom` when it matches none). */\n preset: string;\n name: string;\n family: PaperFamily;\n pattern: GrainPattern;\n /** Grain seed: another seed gives another sheet of the same paper. */\n seed: number;\n /** Paper colour #rrggbb. */\n color: string;\n /** Grain scale: 1 is the pattern's natural size, 2 twice as coarse (0.25..8). */\n scale: number;\n /** How the grain tile repeats: `repeat` or `mirror` (alternate tiles flipped, so seams vanish). */\n tiling: PaperTiling;\n /** Grain depth 0..1: how much grain-responsive dry media skip the valleys (0: none). */\n depth: number;\n /** How much the grain shades the paper colour on screen and in exports, 0..1. */\n shading: number;\n /** The height map as dry media read it. */\n dry: GrainTuning;\n /** The height map as wet media will read it (stored now; wet media is a later phase). */\n wet: GrainTuning;\n /** Wet media: how fast the paper takes up water, 0..1. */\n absorption: number;\n /** Wet media: how far water spreads inside the paper, 0..1. */\n permeability: number;\n /** Wet media: how much the grain catches pigment, 0..1. */\n roughness: number;\n}\n\n/** Side of the generated grain tile in pixels (at scale 1). */\nexport const GRAIN_TILE = 256;\n\nconst unit = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);\nconst clampNum = (v: unknown, lo: number, hi: number, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d);\nconst HEX = /^#[0-9a-f]{6}$/i;\n\ntype PaperPreset = PaperSettings;\n\nconst tuning = (contrast = 1, invert = false): GrainTuning => ({ contrast, invert });\n\nfunction preset(\n id: string,\n name: string,\n family: PaperFamily,\n pattern: GrainPattern,\n color: string,\n o: { depth: number; absorption: number; permeability: number; roughness: number; scale?: number; seed?: number; shading?: number; contrast?: number }\n): PaperPreset {\n return {\n preset: id,\n name,\n family,\n pattern,\n seed: o.seed ?? 1,\n color,\n scale: o.scale ?? 1,\n tiling: 'mirror',\n depth: o.depth,\n shading: o.shading ?? 0.5,\n dry: tuning(o.contrast ?? 1),\n wet: tuning(1),\n absorption: o.absorption,\n permeability: o.permeability,\n roughness: o.roughness,\n };\n}\n\n/** The built-in paper library: generated papers in families. */\nexport const PAPER_LIBRARY: readonly PaperPreset[] = [\n preset('cold-pressed', 'Cold pressed watercolour', 'watercolor', 'cold-pressed', '#f7f3ea', { depth: 0.5, absorption: 0.7, permeability: 0.5, roughness: 0.6 }),\n preset('hot-pressed', 'Hot pressed watercolour', 'watercolor', 'hot-pressed', '#f8f6ef', { depth: 0.25, absorption: 0.55, permeability: 0.4, roughness: 0.25 }),\n preset('rough-watercolor', 'Rough watercolour', 'watercolor', 'rough', '#f5f0e4', { depth: 0.75, absorption: 0.8, permeability: 0.6, roughness: 0.85 }),\n preset('cartridge', 'Cartridge', 'drawing', 'vellum', '#fbfaf6', { depth: 0.3, absorption: 0.4, permeability: 0.3, roughness: 0.35 }),\n preset('laid', 'Laid paper', 'drawing', 'laid', '#f3ecdc', { depth: 0.45, absorption: 0.45, permeability: 0.35, roughness: 0.45 }),\n preset('ingres', 'Charcoal paper', 'drawing', 'ingres', '#ede6d6', { depth: 0.6, absorption: 0.4, permeability: 0.3, roughness: 0.65 }),\n preset('sanded-pastel', 'Sanded pastel paper', 'pastel', 'sanded', '#d8cfc0', { depth: 0.7, absorption: 0.3, permeability: 0.2, roughness: 0.9, shading: 0.35 }),\n preset('pastel-card', 'Grey pastel card', 'pastel', 'sanded', '#9c978e', { depth: 0.55, absorption: 0.3, permeability: 0.2, roughness: 0.75, scale: 1.5, shading: 0.3 }),\n preset('cotton-canvas', 'Cotton canvas', 'canvas', 'canvas', '#efe9dc', { depth: 0.55, absorption: 0.35, permeability: 0.25, roughness: 0.6 }),\n preset('fine-linen', 'Fine linen', 'canvas', 'linen', '#e9e2d2', { depth: 0.45, absorption: 0.3, permeability: 0.25, roughness: 0.5 }),\n preset('gesso', 'Gesso panel', 'panel', 'gesso', '#f6f4ef', { depth: 0.35, absorption: 0.2, permeability: 0.15, roughness: 0.4 }),\n preset('smooth', 'Smooth bristol', 'smooth', 'smooth', '#ffffff', { depth: 0, absorption: 0.3, permeability: 0.2, roughness: 0.05, shading: 0 }),\n];\n\nexport const DEFAULT_PAPER: Readonly<PaperSettings> = PAPER_LIBRARY[0];\n\n/** A paper from the library by id, or `undefined`. */\nexport function paperPreset(id: string): PaperSettings | undefined {\n const p = PAPER_LIBRARY.find((x) => x.preset === id);\n return p ? clonePaper(p) : undefined;\n}\n\nexport function clonePaper(p: PaperSettings): PaperSettings {\n return { ...p, dry: { ...p.dry }, wet: { ...p.wet } };\n}\n\nfunction readTuning(raw: unknown, d: GrainTuning): GrainTuning {\n const r = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};\n return { contrast: clampNum(r.contrast, 0, 3, d.contrast), invert: typeof r.invert === 'boolean' ? r.invert : d.invert };\n}\n\n/**\n * A paper with every field in range. Missing fields come from `base` (default: the library preset\n * named by `raw.preset`, else cold pressed).\n */\nexport function normalizePaper(raw: Partial<PaperSettings> | Record<string, unknown> = {}, base?: PaperSettings): PaperSettings {\n const r = raw as Record<string, unknown>;\n const b = base ?? (typeof r.preset === 'string' ? paperPreset(r.preset) : undefined) ?? clonePaper(DEFAULT_PAPER);\n return {\n preset: typeof r.preset === 'string' && r.preset ? r.preset : b.preset,\n name: typeof r.name === 'string' && r.name.trim() ? r.name.trim() : b.name,\n family: PAPER_FAMILIES.includes(r.family as PaperFamily) ? (r.family as PaperFamily) : b.family,\n pattern: GRAIN_PATTERNS.includes(r.pattern as GrainPattern) ? (r.pattern as GrainPattern) : b.pattern,\n seed: typeof r.seed === 'number' && Number.isFinite(r.seed) ? Math.round(Math.abs(r.seed)) % 2147483647 : b.seed,\n color: typeof r.color === 'string' && HEX.test(r.color) ? r.color.toLowerCase() : b.color,\n scale: clampNum(r.scale, 0.25, 8, b.scale),\n tiling: r.tiling === 'repeat' || r.tiling === 'mirror' ? r.tiling : b.tiling,\n depth: unit(r.depth, b.depth),\n shading: unit(r.shading, b.shading),\n dry: readTuning(r.dry, b.dry),\n wet: readTuning(r.wet, b.wet),\n absorption: unit(r.absorption, b.absorption),\n permeability: unit(r.permeability, b.permeability),\n roughness: unit(r.roughness, b.roughness),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Height maps\n// ---------------------------------------------------------------------------\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/** One pattern's raw height (0..1) at pixel (x, y) of a `n`-pixel tile. */\nfunction patternHeights(pattern: GrainPattern, n: number, seed: number): Float32Array {\n const out = new Float32Array(n * n);\n const salt = Math.imul(seed | 0, 0x9e3779b1) + 17;\n if (pattern === 'smooth') return out.fill(1);\n const noise = (cx: number, cy: number, oct: number, s = 0) => new TileableNoise(n, cx, cy, oct, salt + s);\n const k = n / GRAIN_TILE;\n switch (pattern) {\n case 'cold-pressed': {\n const base = noise(20 * k, 20 * k, 4);\n const fine = noise(64 * k, 64 * k, 2, 3);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 1.8 + 0.5 + (fine.at(x, y) - 0.5) * 0.5 + (hash2(x, y, salt) - 0.5) * 0.12);\n break;\n }\n case 'hot-pressed': {\n const base = noise(40 * k, 40 * k, 3);\n for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 1.1 + 0.6 + (hash2(x, y, salt) - 0.5) * 0.08);\n break;\n }\n case 'rough': {\n const base = noise(10 * k, 10 * k, 5);\n const blot = noise(28 * k, 28 * k, 2, 5);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 2.4 + 0.5 + (blot.at(x, y) - 0.5) * 0.7 + (hash2(x, y, salt) - 0.5) * 0.15);\n break;\n }\n case 'vellum': {\n const base = noise(40 * k, 40 * k, 3);\n for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) out[y * n + x] = clamp01((base.at(x, y) - 0.5) * 1.6 + 0.55 + (hash2(x, y, salt) - 0.5) * 0.2);\n break;\n }\n case 'laid': {\n // Fine laid lines across, chain lines down every eighth of the tile, a little pulp noise.\n const pulp = noise(32 * k, 32 * k, 3);\n const chain = n / 8;\n for (let y = 0; y < n; y++) {\n const laid = 0.5 + 0.5 * Math.cos((2 * Math.PI * y) / 4);\n for (let x = 0; x < n; x++) {\n const c = Math.abs(((x + 0.5) % chain) - chain / 2) / (chain / 2);\n const chainLine = c > 0.94 ? -0.35 : 0;\n out[y * n + x] = clamp01(0.3 + 0.5 * laid + chainLine + (pulp.at(x, y) - 0.5) * 0.6);\n }\n }\n break;\n }\n case 'ingres': {\n // Long vertical fibres with grit.\n const fibres = noise(64 * k, 12 * k, 3);\n const base = noise(16 * k, 16 * k, 3, 7);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) {\n const g = hash2(x, y, salt ^ 0x55);\n out[y * n + x] = clamp01((fibres.at(x, y) - 0.5) * 1.5 + (base.at(x, y) - 0.5) * 0.7 + 0.55 + (g < 0.1 ? -0.3 : 0));\n }\n break;\n }\n case 'sanded': {\n // Fine grit: high-frequency noise and sharp single-pixel pits.\n const grit = noise(96 * k, 96 * k, 2);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) {\n const h = hash2(x, y, salt);\n out[y * n + x] = clamp01((grit.at(x, y) - 0.5) * 2.4 + 0.5 + (h - 0.5) * 0.7);\n }\n break;\n }\n case 'canvas':\n case 'linen': {\n // Over / under weave; linen has finer, irregular threads.\n const period = (pattern === 'canvas' ? 8 : 5) * k;\n // An even thread count keeps the over / under alternation seamless across the tile edge.\n const threads = Math.max(2, 2 * Math.round(n / period / 2));\n const p = n / threads;\n const slub = noise(threads, threads, 2, 11);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) {\n const fx = (x + 0.5) / p;\n const fy = (y + 0.5) / p;\n const cx = Math.floor(fx);\n const cy = Math.floor(fy);\n const warp = Math.sin(Math.PI * (fx - cx));\n const weft = Math.sin(Math.PI * (fy - cy));\n const top = (cx + cy) % 2 === 0 ? warp : weft;\n const irregular = pattern === 'linen' ? (slub.at(x, y) - 0.5) * 0.5 : 0;\n out[y * n + x] = clamp01(0.2 + 0.7 * top + irregular + (hash2(x, y, salt) - 0.5) * 0.15);\n }\n break;\n }\n case 'gesso': {\n // Brushed gesso: soft horizontal streaks over a fine tooth.\n const streaks = noise(2 * k, 28 * k, 3);\n const tooth = noise(64 * k, 64 * k, 2, 13);\n for (let y = 0; y < n; y++)\n for (let x = 0; x < n; x++) out[y * n + x] = clamp01((streaks.at(x, y) - 0.5) * 1.8 + (tooth.at(x, y) - 0.5) * 0.9 + 0.55);\n break;\n }\n }\n return out;\n}\n\nconst rawCache = new Map<string, Float32Array>();\n\n/** The raw height map of a pattern (`GRAIN_TILE` square, 0..1), generated once per pattern and seed. */\nexport function grainHeights(pattern: GrainPattern, seed: number, size = GRAIN_TILE): Float32Array {\n const key = `${pattern}:${seed}:${size}`;\n let h = rawCache.get(key);\n if (!h) {\n h = patternHeights(pattern, size, seed);\n if (rawCache.size > 32) rawCache.delete(rawCache.keys().next().value as string);\n rawCache.set(key, h);\n }\n return h;\n}\n\n/**\n * A paper's height field over the canvas (document pixels), tuned for one kind of media. Read it\n * with `at(x, y)`: bilinear, tiled by `paper.tiling`, at `paper.scale`.\n */\nexport class PaperGrain {\n readonly size = GRAIN_TILE;\n private readonly heights: Float32Array;\n private readonly scale: number;\n private readonly mirror: boolean;\n readonly flat: boolean;\n\n constructor(paper: PaperSettings, media: 'dry' | 'wet' = 'dry') {\n const raw = grainHeights(paper.pattern, paper.seed);\n const t = paper[media];\n this.flat = paper.pattern === 'smooth';\n if (t.contrast === 1 && !t.invert) this.heights = raw;\n else {\n const out = new Float32Array(raw.length);\n for (let i = 0; i < raw.length; i++) {\n const v = clamp01((raw[i] - 0.5) * t.contrast + 0.5);\n out[i] = t.invert ? 1 - v : v;\n }\n this.heights = out;\n }\n this.scale = paper.scale;\n this.mirror = paper.tiling === 'mirror';\n }\n\n /** Tile cell index for a coordinate in grain pixels (mirrored on odd tiles when mirroring). */\n private wrap(v: number): number {\n const n = this.size;\n if (!this.mirror) return ((v % n) + n) % n;\n const p = ((v % (2 * n)) + 2 * n) % (2 * n);\n return p < n ? p : 2 * n - 1 - p;\n }\n\n /** Height 0..1 at document position (x, y). */\n at(x: number, y: number): number {\n const u = x / this.scale - 0.5;\n const v = y / this.scale - 0.5;\n const x0 = Math.floor(u);\n const y0 = Math.floor(v);\n const fx = u - x0;\n const fy = v - y0;\n const n = this.size;\n const h = this.heights;\n const a = this.wrap(x0);\n const b = this.wrap(x0 + 1);\n const c = this.wrap(y0) * n;\n const d = this.wrap(y0 + 1) * n;\n const top = h[c + a] + (h[c + b] - h[c + a]) * fx;\n const bot = h[d + a] + (h[d + b] - h[d + a]) * fx;\n return top + (bot - top) * fy;\n }\n}\n\n/**\n * How much of a dry dab lands at grain height `h` (0..1): the media catches the peaks first, and\n * pressure pushes it into the valleys. `depth` is the paper's grain depth times the brush's grain\n * response; at 0 everything lands (1).\n */\nexport function grainCatch(h: number, depth: number, pressure: number): number {\n if (depth <= 0) return 1;\n const t = depth * (0.75 - 0.65 * Math.min(1, Math.max(0, pressure)));\n const u = clamp01((h - t + 0.18) / 0.36);\n return 1 - depth + depth * u * u * (3 - 2 * u);\n}\n\n/** `#rrggbb` → [r, g, b] 0..255. */\nexport function hexToRgb8(hex: string): [number, number, number] {\n const n = parseInt(hex.slice(1, 7), 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\n/**\n * The paper as opaque RGBA over `rect` (document pixels): its colour, darkened in the valleys by\n * `shading`.\n */\nexport function renderPaper(paper: PaperSettings, rect: { x: number; y: number; width: number; height: number }, grain = new PaperGrain(paper)): Uint8ClampedArray {\n const [r, g, b] = hexToRgb8(paper.color);\n const out = new Uint8ClampedArray(rect.width * rect.height * 4);\n const shade = grain.flat ? 0 : paper.shading * 0.16;\n for (let y = 0; y < rect.height; y++) {\n for (let x = 0; x < rect.width; x++) {\n const i = (y * rect.width + x) * 4;\n const k = shade ? 1 - shade * (1 - grain.at(rect.x + x + 0.5, rect.y + y + 0.5)) : 1;\n out[i] = r * k;\n out[i + 1] = g * k;\n out[i + 2] = b * k;\n out[i + 3] = 255;\n }\n }\n return out;\n}\n","/**\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';\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}\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 * Paint editor brushes: the shared engine's tip settings (`@pixerate/editor/paint`) plus what a\n * natural-media brush adds: paper grain response, pen dynamics (pressure, tilt, barrel rotation,\n * velocity), separate pressure smoothing, a rotation mode, jitter, and Mixer Brush settings for\n * the blending brushes. The built-in presets are our own settings; none come from another product.\n */\n\nimport { DEFAULT_BRUSH, MAX_BRUSH_SIZE, normalizeBrush, type BrushSettings } from '../paint/brush';\nimport { DEFAULT_MIXER, normalizeMixer, type MixerSettings } from '../paint/mixer';\n\n/** How a brush makes marks: dabs into a stroke buffer, removing alpha, or the Mixer Brush engine. */\nexport type PaintBrushEngine = 'dab' | 'eraser' | 'mixer';\n\nexport const PAINT_BRUSH_ENGINES: readonly PaintBrushEngine[] = ['dab', 'eraser', 'mixer'];\n\nexport type PaintBrushCategory = 'pencil' | 'pastel' | 'charcoal' | 'airbrush' | 'marker' | 'ink' | 'eraser' | 'blender' | 'smudge' | 'mixer' | 'custom';\n\nexport const PAINT_BRUSH_CATEGORIES: readonly PaintBrushCategory[] = ['pencil', 'pastel', 'charcoal', 'airbrush', 'marker', 'ink', 'eraser', 'blender', 'smudge', 'mixer', 'custom'];\n\n/** What turns the tip: a fixed angle, the direction of the stroke, or the pen's barrel rotation. */\nexport type BrushRotation = 'fixed' | 'stroke' | 'barrel';\n\nexport const BRUSH_ROTATIONS: readonly BrushRotation[] = ['fixed', 'stroke', 'barrel'];\n\n/** Pen dynamics and jitter. Every amount is 0 (off) .. 1 unless noted. */\nexport interface BrushDynamics {\n /** Size at zero pressure as a fraction of the full size, when pressure controls size (default 0). */\n minSize: number;\n /** How much pressure scales flow (default 0). */\n pressureFlow: number;\n /** Laying the pen down widens the tip, up to 2.5× (default 0). */\n tiltSize: number;\n /** Laying the pen down flattens the tip along the tilt direction (default 0). */\n tiltRoundness: number;\n /** Laying the pen down lightens the mark, as when shading with the side of a pencil (default 0). */\n tiltOpacity: number;\n /** -1..1: positive grows the tip when the pen moves fast, negative thins it (default 0). */\n velocitySize: number;\n /** -1..1: positive darkens fast marks, negative lightens them (default 0). */\n velocityOpacity: number;\n /** Random size change per dab (default 0). */\n sizeJitter: number;\n /** Random angle per dab, as a fraction of ±180° (default 0). */\n angleJitter: number;\n /** Random opacity drop per dab (default 0). */\n opacityJitter: number;\n /** Random offset across the stroke in diameters, 0..4 (default 0). */\n scatter: number;\n}\n\nexport const DEFAULT_DYNAMICS: Readonly<BrushDynamics> = {\n minSize: 0,\n pressureFlow: 0,\n tiltSize: 0,\n tiltRoundness: 0,\n tiltOpacity: 0,\n velocitySize: 0,\n velocityOpacity: 0,\n sizeJitter: 0,\n angleJitter: 0,\n opacityJitter: 0,\n scatter: 0,\n};\n\nexport interface PaintBrush {\n /** Library id (lowercase letters, digits and dashes). */\n id: string;\n name: string;\n category: PaintBrushCategory;\n engine: PaintBrushEngine;\n /** The shared engine's tip: size, hardness, spacing, angle, roundness, opacity, flow, position smoothing, pressure to size / opacity, blend. */\n tip: BrushSettings;\n /** How strongly the mark follows the paper grain, 0 (ignores it) .. 1 (catches only the peaks at light pressure). */\n grain: number;\n /** Pressure smoothing 0..0.95, separate from the tip's position smoothing (default 0). */\n pressureSmoothing: number;\n rotation: BrushRotation;\n dynamics: BrushDynamics;\n /** Mixer engine only: the Mixer Brush settings. */\n mixer: MixerSettings;\n /** Mixer engine only: load the current colour each stroke (false: a smudge or blender that only moves paint). */\n loadsColor: boolean;\n /** True for the built-in presets (they cannot be removed or overwritten). */\n builtin?: boolean;\n}\n\nconst unit = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);\nconst range = (v: unknown, lo: number, hi: number, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d);\n\nexport function normalizeDynamics(raw: Partial<BrushDynamics> = {}, base: BrushDynamics = DEFAULT_DYNAMICS): BrushDynamics {\n return {\n minSize: unit(raw.minSize, base.minSize),\n pressureFlow: unit(raw.pressureFlow, base.pressureFlow),\n tiltSize: unit(raw.tiltSize, base.tiltSize),\n tiltRoundness: unit(raw.tiltRoundness, base.tiltRoundness),\n tiltOpacity: unit(raw.tiltOpacity, base.tiltOpacity),\n velocitySize: range(raw.velocitySize, -1, 1, base.velocitySize),\n velocityOpacity: range(raw.velocityOpacity, -1, 1, base.velocityOpacity),\n sizeJitter: unit(raw.sizeJitter, base.sizeJitter),\n angleJitter: unit(raw.angleJitter, base.angleJitter),\n opacityJitter: unit(raw.opacityJitter, base.opacityJitter),\n scatter: range(raw.scatter, 0, 4, base.scatter),\n };\n}\n\nexport const BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/** A brush with every field in range; missing fields come from `base` (default: a plain round dab brush). */\nexport function normalizePaintBrush(raw: Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer'>> & { tip?: Partial<BrushSettings>; dynamics?: Partial<BrushDynamics>; mixer?: Partial<MixerSettings> } = {}, base?: PaintBrush): PaintBrush {\n const b = base ?? BASE_BRUSH;\n const id = typeof raw.id === 'string' && BRUSH_ID.test(raw.id) ? raw.id : b.id;\n return {\n id,\n name: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,\n category: PAINT_BRUSH_CATEGORIES.includes(raw.category as PaintBrushCategory) ? (raw.category as PaintBrushCategory) : b.category,\n engine: PAINT_BRUSH_ENGINES.includes(raw.engine as PaintBrushEngine) ? (raw.engine as PaintBrushEngine) : b.engine,\n tip: normalizeBrush({ ...b.tip, ...(raw.tip ?? {}) }),\n grain: unit(raw.grain, b.grain),\n pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),\n rotation: BRUSH_ROTATIONS.includes(raw.rotation as BrushRotation) ? (raw.rotation as BrushRotation) : b.rotation,\n dynamics: normalizeDynamics(raw.dynamics ?? {}, b.dynamics),\n mixer: normalizeMixer({ ...b.mixer, ...(raw.mixer ?? {}) }),\n loadsColor: typeof raw.loadsColor === 'boolean' ? raw.loadsColor : b.loadsColor,\n ...(raw.builtin ? { builtin: true } : {}),\n };\n}\n\nconst BASE_BRUSH: PaintBrush = {\n id: 'round',\n name: 'Round',\n category: 'custom',\n engine: 'dab',\n tip: { ...DEFAULT_BRUSH },\n grain: 0,\n pressureSmoothing: 0,\n rotation: 'fixed',\n dynamics: { ...DEFAULT_DYNAMICS },\n mixer: { ...DEFAULT_MIXER },\n loadsColor: true,\n};\n\ntype BrushInit = Parameters<typeof normalizePaintBrush>[0] & { id: string; name: string; category: PaintBrushCategory };\n\nconst builtin = (init: BrushInit): PaintBrush => ({ ...normalizePaintBrush(init), builtin: true });\n\n/** The built-in brush library. */\nexport const BUILTIN_BRUSHES: readonly PaintBrush[] = [\n builtin({\n id: 'pencil-hb', name: 'HB pencil', category: 'pencil',\n tip: { size: 4, hardness: 0.7, spacing: 0.08, opacity: 0.85, flow: 0.55, smoothing: 0.25, pressureSize: true, pressureOpacity: true },\n grain: 0.85, pressureSmoothing: 0.3,\n dynamics: { minSize: 0.55, tiltSize: 0.9, tiltRoundness: 0.7, tiltOpacity: 0.5 },\n }),\n builtin({\n id: 'pencil-6b', name: '6B pencil', category: 'pencil',\n tip: { size: 7, hardness: 0.55, spacing: 0.07, opacity: 1, flow: 0.75, smoothing: 0.2, pressureSize: true, pressureOpacity: true },\n grain: 0.7, pressureSmoothing: 0.3,\n dynamics: { minSize: 0.5, tiltSize: 1, tiltRoundness: 0.75, tiltOpacity: 0.35 },\n }),\n builtin({\n id: 'soft-pastel', name: 'Soft pastel', category: 'pastel', rotation: 'stroke',\n tip: { size: 32, hardness: 0.55, spacing: 0.05, angle: 90, roundness: 0.55, opacity: 1, flow: 0.7, smoothing: 0.15, pressureSize: false, pressureOpacity: true },\n grain: 1, pressureSmoothing: 0.25,\n dynamics: { sizeJitter: 0.06, angleJitter: 0.02 },\n }),\n builtin({\n id: 'oil-pastel', name: 'Oil pastel', category: 'pastel', rotation: 'stroke',\n tip: { size: 26, hardness: 0.8, spacing: 0.05, angle: 90, roundness: 0.7, opacity: 1, flow: 0.9, smoothing: 0.15, pressureSize: true, pressureOpacity: false, blend: 'pigment' },\n grain: 0.5, pressureSmoothing: 0.25,\n dynamics: { minSize: 0.6 },\n }),\n builtin({\n id: 'charcoal-stick', name: 'Charcoal stick', category: 'charcoal', rotation: 'stroke',\n tip: { size: 26, hardness: 0.4, spacing: 0.05, angle: 90, roundness: 0.35, opacity: 0.95, flow: 0.5, smoothing: 0.15, pressureSize: false, pressureOpacity: true },\n grain: 0.95, pressureSmoothing: 0.2,\n dynamics: { sizeJitter: 0.1, angleJitter: 0.03, tiltSize: 0.6 },\n }),\n builtin({\n id: 'vine-charcoal', name: 'Vine charcoal', category: 'charcoal',\n tip: { size: 10, hardness: 0.3, spacing: 0.06, opacity: 0.75, flow: 0.45, smoothing: 0.2, pressureSize: true, pressureOpacity: true },\n grain: 0.9, pressureSmoothing: 0.2,\n dynamics: { minSize: 0.4, tiltSize: 0.8, tiltRoundness: 0.5, tiltOpacity: 0.3, sizeJitter: 0.08 },\n }),\n builtin({\n id: 'airbrush', name: 'Airbrush', category: 'airbrush',\n tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },\n grain: 0, pressureSmoothing: 0.3,\n dynamics: { pressureFlow: 1, tiltSize: 0.5, tiltRoundness: 0.6 },\n }),\n builtin({\n id: 'marker', name: 'Chisel marker', category: 'marker',\n tip: { size: 24, hardness: 0.85, spacing: 0.04, angle: 45, roundness: 0.35, opacity: 0.6, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: false, blend: 'multiply' },\n grain: 0.1,\n }),\n builtin({\n id: 'ink-pen', name: 'Ink pen', category: 'ink',\n tip: { size: 4, hardness: 1, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },\n pressureSmoothing: 0.4,\n dynamics: { minSize: 0.25, velocitySize: -0.45 },\n }),\n builtin({\n id: 'brush-pen', name: 'Brush pen', category: 'ink', rotation: 'stroke',\n tip: { size: 14, hardness: 0.9, spacing: 0.04, roundness: 0.8, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },\n pressureSmoothing: 0.45,\n dynamics: { minSize: 0.05, velocitySize: -0.3 },\n }),\n builtin({\n id: 'eraser-soft', name: 'Soft eraser', category: 'eraser', engine: 'eraser',\n tip: { size: 40, hardness: 0.2, spacing: 0.08, opacity: 1, flow: 0.6, pressureSize: false, pressureOpacity: true },\n }),\n builtin({\n id: 'eraser-hard', name: 'Hard eraser', category: 'eraser', engine: 'eraser',\n tip: { size: 16, hardness: 0.9, spacing: 0.06, opacity: 1, flow: 1, pressureSize: true, pressureOpacity: false },\n grain: 0.3,\n dynamics: { minSize: 0.5 },\n }),\n builtin({\n id: 'blender', name: 'Blender', category: 'blender', engine: 'mixer', loadsColor: false,\n tip: { size: 34, hardness: 0.4, spacing: 0.08, opacity: 1, flow: 0.35, smoothing: 0.2, pressureSize: false, pressureOpacity: true },\n mixer: { wet: 0.7, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: 'pigment' },\n }),\n builtin({\n id: 'smudge', name: 'Smudge', category: 'smudge', engine: 'mixer', loadsColor: false,\n tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow: 0.8, smoothing: 0.2, pressureSize: false, pressureOpacity: true },\n mixer: { wet: 1, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: 'rgb' },\n }),\n builtin({\n id: 'mixer-brush', name: 'Mixer Brush', category: 'mixer', engine: 'mixer',\n tip: { size: 30, hardness: 0.7, spacing: 0.1, opacity: 1, flow: 0.5, smoothing: 0.2, pressureSize: true, pressureOpacity: false },\n mixer: { wet: 0.5, load: 0.95, mix: 0.5, loadEachStroke: true, cleanEachStroke: true, mixing: 'pigment' },\n dynamics: { minSize: 0.5 },\n }),\n];\n\nexport const DEFAULT_BRUSH_ID = 'pencil-6b';\n\nexport { MAX_BRUSH_SIZE };\n\n/** A brush as plain JSON (for export, agents and saved libraries): no `builtin` flag. */\nexport function brushToJSON(brush: PaintBrush): Omit<PaintBrush, 'builtin'> {\n const { builtin: _b, ...rest } = brush;\n return JSON.parse(JSON.stringify(rest));\n}\n\n/** The JSON file a brush library exports to and imports from. */\nexport interface BrushLibraryFile {\n format: 'pixerate-brushes';\n version: 1;\n brushes: Array<Omit<PaintBrush, 'builtin'>>;\n}\n\n/**\n * Brushes read from a brush library file (or a bare array of brushes). Each is normalised; ones\n * without a valid id or name are reported in `problems` and skipped.\n */\nexport function parseBrushLibrary(raw: unknown, problems: string[] = []): PaintBrush[] {\n const list = Array.isArray(raw) ? raw : raw && typeof raw === 'object' && Array.isArray((raw as BrushLibraryFile).brushes) ? (raw as BrushLibraryFile).brushes : null;\n if (!list) throw new Error('Not a brush library: expected {\"format\":\"pixerate-brushes\",\"version\":1,\"brushes\":[...]} or an array of brushes.');\n const out: PaintBrush[] = [];\n list.forEach((item, i) => {\n if (!item || typeof item !== 'object') {\n problems.push(`brushes[${i}] is not an object; skipped.`);\n return;\n }\n const r = item as Record<string, unknown>;\n if (typeof r.id !== 'string' || !BRUSH_ID.test(r.id)) {\n problems.push(`brushes[${i}] has no valid id (lowercase letters, digits and dashes); skipped.`);\n return;\n }\n if (typeof r.name !== 'string' || !r.name.trim()) {\n problems.push(`brushes[${i}] (\"${r.id}\") has no name; skipped.`);\n return;\n }\n const { builtin: _b, ...rest } = r;\n out.push(normalizePaintBrush(rest as Parameters<typeof normalizePaintBrush>[0]));\n });\n return out;\n}\n","/**\n * The paint editor's dry-media stroke engine, on the shared brush engine (`@pixerate/editor/paint`):\n *\n * - Pen input: pressure, tilt (`tiltX` / `tiltY` or `altitude` / `azimuth`), barrel rotation\n * (`twist`) and time (for velocity). Position and pressure are smoothed separately.\n * - `PenDabWalker` walks the points into the shared `Dab`s at `spacing × diameter`, with every\n * pen channel interpolated, and applies the brush's dynamics, rotation mode and jitter.\n * Jitter is a pure hash of the stroke seed and the dab index, so a stroke replays exactly.\n * - `DryStroke` rasterizes each dab with the shared round tip, multiplies it by how much of it the\n * paper grain catches (`grainCatch`), and builds it up in the shared `CoverageMap`; the session\n * then composites the coverage with the shared `applyStrokeToTile`.\n * - Symmetry: one stroke can paint several copies of its points (mirror and kaleidoscope), each\n * with its own walker, into the same coverage, so the copies are one stroke and one undo step.\n *\n * Pure data in, pixels out: no DOM, no framework.\n */\n\nimport { CoverageMap, dabRect, rasterizeDab, type BrushSettings, type Dab, type StrokePoint } from '../paint/brush';\nimport type { RasterRect } from '../raster/surface';\nimport type { PaintBrush } from './brushes';\nimport { randSigned } from './noise';\nimport { grainCatch, type PaperGrain } from './paper';\n\n/** One pen sample in document pixels. Every channel but x and y is optional. */\nexport interface PenPoint {\n x: number;\n y: number;\n /** 0..1 (missing: 1, a mouse). */\n pressure?: number;\n /** Milliseconds since any fixed origin (for velocity; missing: no velocity dynamics). */\n t?: number;\n /** Pointer Events tilt in degrees, -90..90 (used when `altitude` / `azimuth` are missing). */\n tiltX?: number;\n tiltY?: number;\n /** Radians from the surface: π/2 is upright, 0 lies flat. */\n altitude?: number;\n /** Radians clockwise from the +x axis (screen y points down): where the pen leans. */\n azimuth?: number;\n /** Barrel rotation in degrees clockwise, 0..359. */\n twist?: number;\n}\n\n/** A pen's lean as altitude and azimuth, or `null` when the sample has no tilt. */\nexport function penOrientation(p: PenPoint): { altitude: number; azimuth: number } | null {\n if (typeof p.altitude === 'number' && Number.isFinite(p.altitude)) {\n const altitude = Math.min(Math.PI / 2, Math.max(0, p.altitude));\n const azimuth = typeof p.azimuth === 'number' && Number.isFinite(p.azimuth) ? p.azimuth : 0;\n return { altitude, azimuth };\n }\n const hasX = typeof p.tiltX === 'number' && Number.isFinite(p.tiltX);\n const hasY = typeof p.tiltY === 'number' && Number.isFinite(p.tiltY);\n if (!hasX && !hasY) return null;\n return tiltToOrientation(hasX ? p.tiltX! : 0, hasY ? p.tiltY! : 0);\n}\n\n/** Pointer Events `tiltX` / `tiltY` (degrees) → altitude and azimuth (radians). */\nexport function tiltToOrientation(tiltX: number, tiltY: number): { altitude: number; azimuth: number } {\n const clampT = (v: number) => Math.min(89.999, Math.max(-89.999, v));\n const tx = Math.tan((clampT(tiltX) * Math.PI) / 180);\n const ty = Math.tan((clampT(tiltY) * Math.PI) / 180);\n const r = Math.hypot(tx, ty);\n if (r < 1e-9) return { altitude: Math.PI / 2, azimuth: 0 };\n let azimuth = Math.atan2(ty, tx);\n if (azimuth < 0) azimuth += 2 * Math.PI;\n return { altitude: Math.atan(1 / r), azimuth };\n}\n\nconst pressureOf = (p: PenPoint) => (typeof p.pressure === 'number' && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1);\n\n/**\n * Exponential smoothing of position and of pressure, each by its own amount (0..0.95). Feed raw\n * samples; the first sample passes through.\n */\nexport class PenSmoother {\n private pos: { x: number; y: number } | null = null;\n private pressure: number | null = null;\n private readonly a: number;\n private readonly ap: number;\n\n constructor(position: number, pressure: number) {\n this.a = 1 - Math.min(0.95, Math.max(0, position));\n this.ap = 1 - Math.min(0.95, Math.max(0, pressure));\n }\n\n push(p: PenPoint): PenPoint {\n const pr = pressureOf(p);\n if (!this.pos) {\n this.pos = { x: p.x, y: p.y };\n this.pressure = pr;\n return p;\n }\n this.pos = { x: this.pos.x + (p.x - this.pos.x) * this.a, y: this.pos.y + (p.y - this.pos.y) * this.a };\n this.pressure = this.pressure! + (pr - this.pressure!) * this.ap;\n return { ...p, x: this.pos.x, y: this.pos.y, pressure: this.pressure };\n }\n\n /** Where the smoothed position is now (or `null` before the first sample). */\n get position(): { x: number; y: number } | null {\n return this.pos;\n }\n}\n\n/** A linear map of document vectors `[a, b, c, d]`: (x, y) → (a·x + b·y, c·x + d·y). */\nexport type Linear2 = readonly [number, number, number, number];\n\n/** How one copy of a stroke maps the input: a point map and its linear part (for dab angles). */\nexport interface StrokeCopy {\n map(p: PenPoint): PenPoint;\n linear: Linear2;\n}\n\nexport const IDENTITY_COPY: StrokeCopy = { map: (p) => p, linear: [1, 0, 0, 1] };\n\n/** A dab with the pen state it was made at (the shared `Dab` plus pressure for the grain). */\nexport interface PenDab extends Dab {\n pressure: number;\n}\n\nconst enum Stream {\n Size = 1,\n Angle = 2,\n Opacity = 3,\n Scatter = 4,\n}\n\ninterface Sample {\n x: number;\n y: number;\n pressure: number;\n lean: number;\n azimuth: number;\n twist: number | null;\n speed: number | null;\n}\n\n/** Speed (px/ms) at which velocity dynamics reach their full effect. */\nconst FULL_SPEED = 2.5;\n\n/**\n * Walks pen samples into dabs, incrementally (any chunking gives the same dabs). Dab angles are in\n * the shared rasterizer's convention: radians, counter-clockwise with y up.\n */\nexport class PenDabWalker {\n private last: Sample | null = null;\n private nextAt = 0;\n private pendingFirst: Sample | null = null;\n private index = 0;\n private dir = 0;\n private lastT: number | null = null;\n private speed: number | null = null;\n\n constructor(\n readonly brush: PaintBrush,\n private readonly seed = 0,\n private readonly linear: Linear2 = [1, 0, 0, 1]\n ) {}\n\n private sample(p: PenPoint): Sample {\n const o = penOrientation(p);\n let speed: number | null = null;\n if (typeof p.t === 'number' && Number.isFinite(p.t)) {\n if (this.last && this.lastT !== null && p.t > this.lastT) {\n const v = Math.hypot(p.x - this.last.x, p.y - this.last.y) / (p.t - this.lastT);\n this.speed = this.speed === null ? v : this.speed + (v - this.speed) * 0.35;\n }\n this.lastT = p.t;\n speed = this.speed ?? 0;\n }\n return {\n x: p.x,\n y: p.y,\n pressure: pressureOf(p),\n lean: o ? 1 - o.altitude / (Math.PI / 2) : 0,\n azimuth: o ? o.azimuth : 0,\n twist: typeof p.twist === 'number' && Number.isFinite(p.twist) ? p.twist : null,\n speed,\n };\n }\n\n private diameter(s: Sample): number {\n const tip = this.brush.tip;\n const dyn = this.brush.dynamics;\n let d = tip.size;\n if (tip.pressureSize) d *= dyn.minSize + (1 - dyn.minSize) * s.pressure;\n d *= 1 + dyn.tiltSize * s.lean * 1.5;\n if (s.speed !== null && dyn.velocitySize) d *= Math.max(0.05, 1 + dyn.velocitySize * Math.min(1, s.speed / FULL_SPEED) * (dyn.velocitySize < 0 ? 0.85 : 1));\n return d;\n }\n\n private stepLen(s: Sample): number {\n return Math.max(0.5, Math.max(1, this.diameter(s)) * Math.max(0.01, this.brush.tip.spacing));\n }\n\n /** A rasterizer angle (CCW, y up) mapped through this copy's linear part. */\n private mapAngle(theta: number): number {\n const [a, b, c, d] = this.linear;\n if (a === 1 && b === 0 && c === 0 && d === 1) return theta;\n const vx = Math.cos(theta);\n const vy = -Math.sin(theta);\n return Math.atan2(-(c * vx + d * vy), a * vx + b * vy);\n }\n\n private dab(s: Sample): PenDab {\n const { tip, dynamics: dyn, rotation } = this.brush;\n const i = this.index++;\n const seed = this.seed;\n let diameter = this.diameter(s);\n if (dyn.sizeJitter) diameter *= Math.max(0.05, 1 + dyn.sizeJitter * randSigned(seed, i, Stream.Size));\n const base = (tip.angle * Math.PI) / 180;\n let angle: number;\n if (rotation === 'stroke') angle = this.dir + base;\n else if (rotation === 'barrel' && s.twist !== null) angle = this.mapAngle(base - (s.twist * Math.PI) / 180);\n else if (dyn.tiltRoundness > 0 && s.lean > 0.02) angle = this.mapAngle(base - s.azimuth);\n else angle = this.mapAngle(base);\n if (dyn.angleJitter) angle += dyn.angleJitter * Math.PI * randSigned(seed, i, Stream.Angle);\n let opacity = tip.pressureOpacity ? s.pressure : 1;\n opacity *= 1 - dyn.tiltOpacity * s.lean * 0.8;\n if (s.speed !== null && dyn.velocityOpacity) opacity *= Math.max(0, 1 + dyn.velocityOpacity * (Math.min(1, s.speed / FULL_SPEED) - 0.5));\n if (dyn.opacityJitter) opacity *= 1 - dyn.opacityJitter * (randSigned(seed, i, Stream.Opacity) * 0.5 + 0.5);\n const flow = tip.flow * (1 - dyn.pressureFlow + dyn.pressureFlow * s.pressure);\n let { x, y } = s;\n if (dyn.scatter) {\n const off = dyn.scatter * diameter * randSigned(seed, i, Stream.Scatter);\n // Across the stroke: the direction of travel turned a quarter (document y points down).\n x += Math.sin(this.dir) * off;\n y += Math.cos(this.dir) * off;\n }\n return {\n x,\n y,\n radius: Math.max(0.5, diameter / 2),\n alpha: Math.min(1, Math.max(0, flow)),\n opacity: Math.min(1, Math.max(0, opacity)),\n angle,\n roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),\n pressure: s.pressure,\n };\n }\n\n push(p: PenPoint, out: PenDab[]): void {\n const a = this.last;\n const s = this.sample(p);\n if (!a) {\n this.last = s;\n this.pendingFirst = s;\n this.nextAt = this.stepLen(s);\n return;\n }\n const dx = s.x - a.x;\n const dy = s.y - a.y;\n const len = Math.hypot(dx, dy);\n if (len > 1e-9) this.dir = Math.atan2(-dy, dx);\n if (this.pendingFirst) {\n out.push(this.dab(this.pendingFirst));\n this.pendingFirst = null;\n }\n if (len > 1e-9) {\n while (this.nextAt <= len + 1e-9) {\n const f = Math.min(1, this.nextAt / len);\n const lerp = (u: number, v: number) => u + (v - u) * f;\n const q: Sample = {\n x: a.x + dx * f,\n y: a.y + dy * f,\n pressure: lerp(a.pressure, s.pressure),\n lean: lerp(a.lean, s.lean),\n azimuth: lerpAngle(a.azimuth, s.azimuth, f),\n twist: a.twist !== null && s.twist !== null ? lerpDegrees(a.twist, s.twist, f) : s.twist,\n speed: a.speed !== null && s.speed !== null ? lerp(a.speed, s.speed) : s.speed,\n };\n out.push(this.dab(q));\n this.nextAt += this.stepLen(q);\n }\n this.nextAt -= len;\n }\n this.last = s;\n }\n\n finish(out: PenDab[]): void {\n if (this.pendingFirst) {\n out.push(this.dab(this.pendingFirst));\n this.pendingFirst = null;\n }\n }\n}\n\nfunction lerpAngle(a: number, b: number, t: number): number {\n let d = b - a;\n while (d > Math.PI) d -= 2 * Math.PI;\n while (d < -Math.PI) d += 2 * Math.PI;\n return a + d * t;\n}\n\nfunction lerpDegrees(a: number, b: number, t: number): number {\n let d = b - a;\n while (d > 180) d -= 360;\n while (d < -180) d += 360;\n return a + d * t;\n}\n\nexport interface DryStrokeOptions {\n /** The paper's dry height field, or `null` for no grain. */\n grain: PaperGrain | null;\n /** The paper's grain depth, 0..1 (multiplied by the brush's grain response). */\n paperDepth: number;\n /** Jitter seed (record it to replay the stroke exactly). */\n seed?: number;\n /** Copies of the stroke (symmetry); default one identity copy. */\n copies?: readonly StrokeCopy[];\n}\n\n/**\n * One dab-engine stroke (dab and eraser brushes): smoothing, the dab walk of every copy, grain and\n * build-up in a shared coverage map.\n */\nexport class DryStroke {\n readonly coverage = new CoverageMap();\n dabCount = 0;\n private readonly smoother: PenSmoother;\n private readonly walkers: PenDabWalker[];\n private readonly copies: readonly StrokeCopy[];\n private readonly depth: number;\n private buf = new Float32Array(0);\n private lastRaw: PenPoint | null = null;\n private lastFed: PenPoint | null = null;\n\n constructor(readonly brush: PaintBrush, private readonly options: DryStrokeOptions) {\n this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);\n this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];\n this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, ((options.seed ?? 0) + i * 7919) | 0, c.linear));\n this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;\n }\n\n /** Feeds raw pen samples; returns the document rectangles the new dabs touched. */\n push(points: readonly PenPoint[]): RasterRect[] {\n const dabs: PenDab[] = [];\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) => this.walkers[i].push(c.map(p), dabs));\n }\n return this.paint(dabs);\n }\n\n /** Ends the stroke: the smoothed path catches up with the last raw sample; a single tap paints one dab. */\n finish(): RasterRect[] {\n const dabs: PenDab[] = [];\n const raw = this.lastRaw;\n const fed = this.lastFed;\n if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.copies.forEach((c, i) => this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs));\n for (const w of this.walkers) w.finish(dabs);\n return this.paint(dabs);\n }\n\n private paint(dabs: PenDab[]): RasterRect[] {\n const rects: RasterRect[] = [];\n const hardness = this.brush.tip.hardness;\n const grain = this.options.grain;\n for (const d of dabs) {\n const rect = dabRect(d, hardness);\n const n = rect.width * rect.height;\n if (this.buf.length < n) this.buf = new Float32Array(n);\n const vals = this.buf.subarray(0, n);\n rasterizeDab(d, hardness, rect, vals);\n if (this.depth > 0 && 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++) {\n if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);\n }\n }\n }\n this.coverage.accumulate(rect, vals, d.opacity);\n rects.push(rect);\n this.dabCount++;\n }\n return rects;\n }\n}\n\n/** The shared engine's tip for the Mixer Brush engine (it smooths and walks by itself, so smoothing is applied before). */\nexport function mixerTip(brush: PaintBrush): BrushSettings {\n return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };\n}\n\n/** Pen samples → the shared engine's stroke points (pressure only). */\nexport function toStrokePoint(p: PenPoint): StrokePoint {\n return { x: p.x, y: p.y, pressure: pressureOf(p), ...(typeof p.t === 'number' ? { t: p.t } : {}) };\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 * - Mixer engine brushes (Mixer Brush, blender, smudge): dabs read what earlier dabs laid down, so\n * they paint into live tile copies; every tile written since the last update gets a new `Tile`\n * object, because the compositor caches by tile identity.\n */\n\nimport { applyStrokeToTile, type StrokeApply } from '../paint/brush';\nimport { MixerStroke, cleanMixer, loadMixer, type MixerLoad, type MixerTarget } from '../paint/mixer';\nimport { TILE, TiledSurface, finishTile, tileKey, tilesInRect, type RasterRect, type Tile, type TileKey } from '../raster/surface';\nimport type { PaintBrush } from './brushes';\nimport { DryStroke, IDENTITY_COPY, PenSmoother, mixerTip, toStrokePoint, type PenPoint, type StrokeCopy } from './engine';\nimport type { PaperGrain } from './paper';\nimport type { Affine } from './strokes';\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 /** Mixer engine: the brush load at the start of the stroke. */\n mixerLoad: MixerLoad;\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 engine brushes: what the brush carries after the stroke. */\n mixerLoad?: MixerLoad;\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: DryStroke | null = null;\n private readonly mixers: MixerStroke[] = [];\n private readonly mixerSmoother: PenSmoother | null = null;\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\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 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: null,\n offset: init.offset,\n };\n if (brush.engine === 'mixer') {\n this.mixerSmoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);\n const load = init.mixerLoad;\n for (let i = 0; i < this.copies.length; i++) this.mixers.push(new MixerStroke(mixerTip(brush), brush.mixer, load, this.mixerTarget()));\n } else {\n this.dry = new DryStroke(brush, { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies });\n }\n }\n\n private mixerTarget(): MixerTarget {\n return {\n offset: this.init.offset,\n lockTransparency: this.init.lockTransparency,\n 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 for (const p of points) this.points.push(p);\n this.lastRaw = points[points.length - 1];\n if (this.dry) this.update(this.dry.push(points));\n else {\n const rects: RasterRect[] = [];\n for (const raw of points) {\n const p = toStrokePoint(this.mixerSmoother!.push(raw));\n this.copies.forEach((c, i) => rects.push(...this.mixers[i].push([toStrokePoint(c.map(p))])));\n }\n this.update(rects);\n }\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.dry) this.update(this.dry.finish());\n else {\n const rects: RasterRect[] = [];\n const last = this.lastRaw ? toStrokePoint(this.lastRaw) : undefined;\n this.copies.forEach((c, i) => rects.push(...this.mixers[i].finish(last ? toStrokePoint(c.map(last)) : undefined)));\n this.update(rects);\n }\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 const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;\n const dabs = this.dry ? this.dry.dabCount : this.mixers.reduce((n, m) => n + m.dabCount, 0);\n this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };\n if (this.mixers.length) {\n const brush = this.init.brush;\n let load = this.mixers[0].load;\n if (brush.mixer.cleanEachStroke) load = cleanMixer(load);\n this.result.mixerLoad = load;\n }\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 * 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 PaintBrush } from './brushes';\nimport type { Linear2, PenPoint, StrokeCopy } from './engine';\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}\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\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 Parameters<typeof normalizePaintBrush>[0]) ?? {}),\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 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.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}\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): string {\n return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry]);\n}\n\nconst paperNodes = new WeakMap<PaperSettings, PaperNode>();\n\n/** The paper as a compositor layer (one object per paper settings object, so caches hold). */\nexport function paperNode(paper: PaperSettings): PaperNode {\n let n = paperNodes.get(paper);\n if (!n) {\n n = { type: 'paper', id: PAPER_NODE_ID, name: 'Paper', visible: true, opacity: 1, blend: 'normal', paper };\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): Uint8ClampedArray {\n const key = `${paperKey(paper)}|${r.x},${r.y},${r.width},${r.height}`;\n let t = tileCache.get(key);\n if (!t) {\n t = renderPaper(paper, r, paperGrain(paper));\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);\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);\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 PaintLayer } from './document';\nimport { normalizePaper, type PaperSettings } from './paper';\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 is never saved (saving dries it), so there is no wet field.\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']);\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}\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 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 /** 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 return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), 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';\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).\n */\nexport function samplePressure(e: PointerSample): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * 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 * 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 { mixerLoadForStroke, PaintStrokeSession } from './stroke';\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 const session = new PaintStrokeSession(\n { update: () => {}, commit: (_s, s) => (result = s ?? result) },\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: brush.engine === 'mixer' ? mixerLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]) : EMPTY_MIXER_LOAD,\n }\n );\n session.push(thumbnailStroke(width, height));\n session.end();\n return 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 === 'mixer' && !b.loadsColor) {\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 = 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","/**\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 { isBlendMode, type BlendMode } from '../raster/blend';\nimport { findLayer, flattenLayers, insertLayer, isLayerLocked, mapLayer, type LayerLocks } from '../raster/layers';\nimport { bytesToBase64, encodePng } from '../raster/png';\nimport {\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, 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} 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, 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 { mixerLoadForStroke, 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 { DEFAULT_VIEW, normalizeView, type PaintView } from './view';\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}\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 /** 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 /** A stroke is in progress. */\n stroking: boolean;\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 /** Default the active brush. */\n brushId?: string;\n /** Changes to the brush for this stroke only. */\n brush?: Parameters<typeof normalizePaintBrush>[0];\n /** Jitter seed (default: a new one per stroke). */\n seed?: number;\n /** Extra copies of the stroke as affine maps (mirror / kaleidoscope). */\n copies?: readonly Affine[];\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\nexport interface PixelImage {\n width: number;\n height: number;\n data: Uint8ClampedArray;\n}\n\ninterface HistoryEntry {\n label: string;\n before: PaintDocumentState;\n after: PaintDocumentState;\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 private color: string;\n private secondaryColor = '#ffffff';\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 readonly viewCompositor = createPaintCompositor();\n private readonly exportCompositor = createPaintCompositor();\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 }\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: [...BUILTIN_BRUSHES, ...this.userBrushes],\n color: this.color,\n secondaryColor: this.secondaryColor,\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 stroking: Boolean(this.session),\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): void {\n this.undoStack.push({ label, before: this.doc, after: next });\n if (this.undoStack.length > this.historyLimit) this.undoStack.shift();\n this.redoStack = [];\n this.doc = next;\n this.emit();\n }\n\n undo(): string | null {\n this.requireIdle('undoing');\n const e = this.undoStack.pop();\n if (!e) return null;\n this.redoStack.push(e);\n this.doc = e.before;\n this.emit();\n return e.label;\n }\n\n redo(): string | null {\n this.requireIdle('redoing');\n const e = this.redoStack.pop();\n if (!e) return null;\n this.undoStack.push(e);\n this.doc = e.after;\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.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 this.commit('Canvas size', { ...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 this.commit('Paper', { ...this.doc, paper });\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 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 });\n return { removed: id, activeLayerId: active };\n }\n\n duplicateLayer(id: string): PaintLayerInfo {\n this.requireIdle('duplicating 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 copy = (l: PaintLayer): PaintLayer => (l.type === 'group' ? { ...l, id: newPaintLayerId(), children: l.children.map(copy) } : { ...l, id: newPaintLayerId() });\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.doc, layers, activeLayerId: dup.id });\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 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 const px = createPaintCompositor().composite([base, upper.visible ? upper : { ...upper, 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.doc, layers, activeLayerId: 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 const bounds = this.describeLayer(l).bounds;\n this.commit('Clear layer', { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })) });\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 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.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })) });\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<Parameters<typeof normalizePaintBrush>[0], 'id'> = {}): PaintBrush {\n const { id, ...changes } = update;\n const base = id !== undefined ? this.requireBrush(id) : this.brush;\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?: Parameters<typeof normalizePaintBrush>[0] }): 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.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.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) this.emit([]);\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 setColor(color: string): string {\n this.color = parseColor(color);\n this.emit([]);\n return this.color;\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 /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */\n protected strokeCopies(): Affine[] {\n return [];\n }\n\n private sessionFor(doc: PaintDocumentState, init: PaintStrokeInit, host: StrokeSessionHost): PaintStrokeSession {\n const 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 color = init.color === undefined ? this.color : parseColor(init.color);\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 mixerLoad: brush.engine === 'mixer' ? mixerLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]) : EMPTY_MIXER_LOAD,\n });\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 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 host: StrokeSessionHost = {\n update: (layerId, surface, rects) => {\n this.live = { layerId, surface };\n this.emit(rects);\n },\n commit: (s, surface, cancelled) => {\n this.session = null;\n this.live = null;\n this.liveLayers = null;\n const brush = s.init.brush;\n if (!cancelled && s.result?.mixerLoad && brush.loadsColor) this.mixerLoad = s.result.mixerLoad;\n if (cancelled || !surface) {\n this.emit();\n return;\n }\n const stroke = this.record(s);\n s.result!.strokeId = stroke.id;\n const l = this.getLayer(s.layerId) as PaintPixelLayer;\n this.commit(`${brush.name} stroke`, { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke] });\n },\n };\n const session = this.sessionFor(this.doc, init, host);\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 out = { ...out, layers: mapLayer(out.layers, l.id, () => ({ ...l, surface })), strokes: [...out.strokes, stroke] };\n },\n };\n const session = this.sessionFor(doc, init, host);\n session.push(points);\n const result = session.end();\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 { doc, result, brush } = this.runStroke(this.doc, init, points);\n if (result.mixerLoad && brush.loadsColor) this.mixerLoad = result.mixerLoad;\n if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);\n else this.emit([]);\n return result;\n }\n\n private 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 const run = this.runStroke(doc, { layerId, color: options.color ?? s.color, brush, seed: s.seed, copies: v.copies ?? [] }, 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 // --- 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 return [];\n }\n\n private renderLayers(options: RenderOptions, view = false): ViewLayer[] {\n let layers: PaintLayer[] = this.currentLayers();\n if (options.layerIds) {\n const picked: PaintLayer[] = [];\n for (const id of options.layerIds) picked.push(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)];\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), { 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);\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 toJSON(options: { strokes?: boolean } = {}): SerializedPaintDocument {\n return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions });\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.doc, store, { strokes: options.strokes, extensions: this.extensions });\n }\n\n private adopt(loaded: { state: PaintDocumentState; extensions: PaintDocumentExtensions; problems: string[] }): string[] {\n this.extensions = loaded.extensions;\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 = loaded.state;\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 { 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 const result = exportPsd({ width, height, layers: [paperLayer, ...this.doc.layers] as unknown as ImageLayer[], merged, resolution: dpi });\n const pigment = flattenLayers(this.doc.layers).filter((l) => l.blend === 'pigment');\n if (pigment.length) result.warnings.push(`Pigment blending has no PSD equivalent; ${pigment.map((l) => `\"${l.name}\"`).join(', ')} ${pigment.length === 1 ? 'was' : 'were'} written as Normal, so ${pigment.length === 1 ? 'it looks' : 'they look'} different in Photoshop.`);\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 /** 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 * 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 { BLEND_MODES, type BlendMode } from '../raster/blend';\nimport { bytesToBase64, encodePng } from '../raster/png';\nimport { BRUSH_ROTATIONS, MAX_BRUSH_SIZE, PAINT_BRUSH_CATEGORIES, PAINT_BRUSH_ENGINES, brushToJSON, 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';\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\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: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer. pigment mixes it in like real paint (blue into yellow gives green); multiply darkens like marker ink.' },\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 (Mixer Brush, blender, smudge). Missing fields keep their current values.',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: '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: \"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 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: { type: 'string', enum: [...PAINT_BRUSH_ENGINES], description: 'dab: paints dabs; eraser: removes paint; mixer: picks up and moves paint (Mixer Brush, blender, smudge).' },\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 blender and smudge, which only move 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 { id: b.id, name: b.name, category: b.category, engine: b.engine, size: b.tip.size, grain: b.grain, rotation: b.rotation, builtin: Boolean(b.builtin) };\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\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 };\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 (stored for later).' },\n absorption: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how fast the paper takes water (stored for later).' },\n permeability: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how far water spreads in the paper (stored for later).' },\n roughness: { type: 'number', minimum: 0, maximum: 1, description: 'Wet media: how much the grain catches pigment (stored for later).' },\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: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (pass-through for groups only).' },\n clipped: { type: 'boolean', description: 'Clip to the 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: '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).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill a layer', ...WRITE },\n handler: (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, blender, smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response and rotation 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: 'Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer). Without id, the active brush as edited.',\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 if (id === undefined) return brushToJSON(c.getState().brush);\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) };\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: '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: \"Reads what the Mixer Brush carries between strokes: reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null. Call before painting with mixer-brush when its loadEachStroke is false.\",\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: 'Empties the Mixer Brush pickup well (and with emptyReservoir its 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, blender, smudge) picks up and moves paint already on the layer. The painting guides add their mirror copies. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes) and, for the mixer, its load 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 seed: { type: 'integer', minimum: 0, description: 'Jitter seed (default a new one); the same seed and points give the same marks.' },\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (a) => {\n const r = c.paintStroke({ points: a.points as PenPoint[], layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, seed: a.seed });\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) => c.replayStrokes(a as Parameters<PaintEditorController['replayStrokes']>[0]),\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 blending has no PSD equivalent).',\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(...createPigmentTools());\n\n return { id: options.id ?? 'paint-editor', namespace: options.namespace, tools };\n}\n"]}