@pixerate/editor 0.101.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 (67) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -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-WIR377F6.js → chunk-67FRIOPZ.js} +15 -12
  9. package/dist/chunk-67FRIOPZ.js.map +1 -0
  10. package/dist/chunk-6BU5WU4R.cjs +1 -0
  11. package/dist/chunk-6BU5WU4R.cjs.map +1 -0
  12. package/dist/{chunk-IB53LVGV.js → chunk-DS7KDKPX.js} +1 -1
  13. package/dist/chunk-NQLQ2JOI.js +7176 -0
  14. package/dist/chunk-NQLQ2JOI.js.map +1 -0
  15. package/dist/{chunk-ZZOTHJ4K.cjs → chunk-PV42JITT.cjs} +1 -1
  16. package/dist/{chunk-ZZOTHJ4K.cjs.map → chunk-PV42JITT.cjs.map} +1 -1
  17. package/dist/chunk-VSMM7B3P.cjs +7176 -0
  18. package/dist/chunk-VSMM7B3P.cjs.map +1 -0
  19. package/dist/{chunk-WT6ZAWJP.cjs → chunk-WSTEXEKD.cjs} +27 -24
  20. package/dist/chunk-WSTEXEKD.cjs.map +1 -0
  21. package/dist/chunk-XYBJKUAU.js +1 -0
  22. package/dist/chunk-XYBJKUAU.js.map +1 -0
  23. package/dist/compositor-B1ZrKR89.d.ts +835 -0
  24. package/dist/compositor-CTMU-B1t.d.cts +835 -0
  25. package/dist/export-BLpMyCYg.d.ts +3139 -0
  26. package/dist/export-X9rY79p0.d.cts +3139 -0
  27. package/dist/homography-CZ-tFjay.d.cts +50 -0
  28. package/dist/homography-Ctfjoc1b.d.ts +50 -0
  29. package/dist/image-editor/index.cjs +3889 -8657
  30. package/dist/image-editor/index.cjs.map +1 -1
  31. package/dist/image-editor/index.d.cts +224 -2723
  32. package/dist/image-editor/index.d.ts +224 -2723
  33. package/dist/image-editor/index.js +3326 -8094
  34. package/dist/image-editor/index.js.map +1 -1
  35. package/dist/mixer-DG_9Yu2W.d.ts +282 -0
  36. package/dist/mixer-DGcH6HLu.d.cts +282 -0
  37. package/dist/paint/index.cjs +4 -3
  38. package/dist/paint/index.cjs.map +1 -1
  39. package/dist/paint/index.d.cts +3 -281
  40. package/dist/paint/index.d.ts +3 -281
  41. package/dist/paint/index.js +2 -1
  42. package/dist/paint-editor/index.cjs +3330 -0
  43. package/dist/paint-editor/index.cjs.map +1 -0
  44. package/dist/paint-editor/index.d.cts +1233 -0
  45. package/dist/paint-editor/index.d.ts +1233 -0
  46. package/dist/paint-editor/index.js +3330 -0
  47. package/dist/paint-editor/index.js.map +1 -0
  48. package/dist/raster/index.cjs +2 -2
  49. package/dist/raster/index.d.cts +6 -827
  50. package/dist/raster/index.d.ts +6 -827
  51. package/dist/raster/index.js +1 -1
  52. package/dist/{surface-DIA-XatS.d.cts → surface-AfHCH7BR.d.cts} +1 -1
  53. package/dist/{surface-DIA-XatS.d.ts → surface-AfHCH7BR.d.ts} +1 -1
  54. package/dist/tools/index.cjs +3 -3
  55. package/dist/tools/index.cjs.map +1 -1
  56. package/dist/tools/index.js +3 -3
  57. package/dist/vector/index.d.cts +2 -1
  58. package/dist/vector/index.d.ts +2 -1
  59. package/dist/{warp-B8rAguzs.d.ts → warp-CWHZzE3x.d.ts} +2 -49
  60. package/dist/{warp-nYs2DPrx.d.cts → warp-D5jz_tN7.d.cts} +2 -49
  61. package/package.json +6 -1
  62. package/skills/README.md +1 -0
  63. package/skills/pixerate-image-editor/SKILL.md +69 -11
  64. package/skills/pixerate-paint-editor/SKILL.md +162 -0
  65. package/dist/chunk-WIR377F6.js.map +0 -1
  66. package/dist/chunk-WT6ZAWJP.cjs.map +0 -1
  67. /package/dist/{chunk-IB53LVGV.js.map → chunk-DS7KDKPX.js.map} +0 -0
@@ -0,0 +1,3330 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7;
2
+
3
+
4
+
5
+
6
+
7
+ var _chunkVSMM7B3Pcjs = require('../chunk-VSMM7B3P.cjs');
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+ var _chunkWSTEXEKDcjs = require('../chunk-WSTEXEKD.cjs');
18
+ require('../chunk-MUUXNRU4.cjs');
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+ var _chunkPV42JITTcjs = require('../chunk-PV42JITT.cjs');
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+ var _chunkCRMAXIUVcjs = require('../chunk-CRMAXIUV.cjs');
50
+ require('../chunk-DWGZUSES.cjs');
51
+
52
+ // src/paint-editor/noise.ts
53
+ function mix32(h) {
54
+ h ^= h >>> 16;
55
+ h = Math.imul(h, 2246822507);
56
+ h ^= h >>> 13;
57
+ h = Math.imul(h, 3266489909);
58
+ h ^= h >>> 16;
59
+ return h >>> 0;
60
+ }
61
+ function hash2(x, y, salt) {
62
+ const h = mix32((Math.imul(x | 0, 668265261) ^ mix32((y | 0) + Math.imul(salt | 0, 374761393))) >>> 0);
63
+ return h / 4294967296;
64
+ }
65
+ function rand01(seed, index, stream) {
66
+ return hash2(index, stream, seed);
67
+ }
68
+ function randSigned(seed, index, stream) {
69
+ return rand01(seed, index, stream) * 2 - 1;
70
+ }
71
+ function seedFromString(text) {
72
+ let h = 2166136261;
73
+ for (let i = 0; i < text.length; i++) {
74
+ h ^= text.charCodeAt(i);
75
+ h = Math.imul(h, 16777619);
76
+ }
77
+ return mix32(h >>> 0);
78
+ }
79
+ var smooth = (t) => t * t * (3 - 2 * t);
80
+ var Lattice = class {
81
+ constructor(px, py, salt) {
82
+ this.px = px;
83
+ this.py = py;
84
+ this.values = new Float32Array(px * py);
85
+ for (let j = 0; j < py; j++) for (let i = 0; i < px; i++) this.values[j * px + i] = hash2(i, j, salt);
86
+ }
87
+
88
+
89
+
90
+ /** Value noise at (x, y) in lattice cells; wraps every `px` / `py` cells. */
91
+ at(x, y) {
92
+ const { px, py } = this;
93
+ const x0 = Math.floor(x);
94
+ const y0 = Math.floor(y);
95
+ const tx = smooth(x - x0);
96
+ const ty = smooth(y - y0);
97
+ const i0 = (x0 % px + px) % px;
98
+ const j0 = (y0 % py + py) % py;
99
+ const i1 = (i0 + 1) % px;
100
+ const j1 = (j0 + 1) % py;
101
+ const v = this.values;
102
+ const a = v[j0 * px + i0] + (v[j0 * px + i1] - v[j0 * px + i0]) * tx;
103
+ const b = v[j1 * px + i0] + (v[j1 * px + i1] - v[j1 * px + i0]) * tx;
104
+ return a + (b - a) * ty;
105
+ }
106
+ };
107
+ var TileableNoise = (_class = class {
108
+ constructor(size, cellsX, cellsY, octaves, salt) {;_class.prototype.__init.call(this);
109
+ this.size = size;
110
+ let cx = Math.max(1, Math.round(cellsX));
111
+ let cy = Math.max(1, Math.round(cellsY));
112
+ for (let o = 0; o < octaves; o++) {
113
+ this.octaves.push(new Lattice(cx, cy, salt + o * 7919 | 0));
114
+ cx *= 2;
115
+ cy *= 2;
116
+ }
117
+ }
118
+
119
+ __init() {this.octaves = []}
120
+ /** Fractal noise at pixel (x, y). */
121
+ at(x, y) {
122
+ let sum = 0;
123
+ let amp = 1;
124
+ let norm = 0;
125
+ const u = x / this.size;
126
+ const v = y / this.size;
127
+ for (const l of this.octaves) {
128
+ sum += l.at(u * l.px, v * l.py) * amp;
129
+ norm += amp;
130
+ amp *= 0.5;
131
+ }
132
+ return norm > 0 ? sum / norm : 0.5;
133
+ }
134
+ }, _class);
135
+
136
+ // src/paint-editor/paper.ts
137
+ var GRAIN_PATTERNS = ["smooth", "cold-pressed", "hot-pressed", "rough", "vellum", "laid", "ingres", "sanded", "canvas", "linen", "gesso"];
138
+ var PAPER_FAMILIES = ["watercolor", "drawing", "pastel", "canvas", "panel", "smooth"];
139
+ var GRAIN_TILE = 256;
140
+ var unit = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
141
+ var clampNum = (v, lo, hi, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d;
142
+ var HEX = /^#[0-9a-f]{6}$/i;
143
+ var tuning = (contrast = 1, invert = false) => ({ contrast, invert });
144
+ function preset(id, name, family, pattern, color, o) {
145
+ return {
146
+ preset: id,
147
+ name,
148
+ family,
149
+ pattern,
150
+ seed: _nullishCoalesce(o.seed, () => ( 1)),
151
+ color,
152
+ scale: _nullishCoalesce(o.scale, () => ( 1)),
153
+ tiling: "mirror",
154
+ depth: o.depth,
155
+ shading: _nullishCoalesce(o.shading, () => ( 0.5)),
156
+ dry: tuning(_nullishCoalesce(o.contrast, () => ( 1))),
157
+ wet: tuning(1),
158
+ absorption: o.absorption,
159
+ permeability: o.permeability,
160
+ roughness: o.roughness
161
+ };
162
+ }
163
+ var PAPER_LIBRARY = [
164
+ preset("cold-pressed", "Cold pressed watercolour", "watercolor", "cold-pressed", "#f7f3ea", { depth: 0.5, absorption: 0.7, permeability: 0.5, roughness: 0.6 }),
165
+ preset("hot-pressed", "Hot pressed watercolour", "watercolor", "hot-pressed", "#f8f6ef", { depth: 0.25, absorption: 0.55, permeability: 0.4, roughness: 0.25 }),
166
+ preset("rough-watercolor", "Rough watercolour", "watercolor", "rough", "#f5f0e4", { depth: 0.75, absorption: 0.8, permeability: 0.6, roughness: 0.85 }),
167
+ preset("cartridge", "Cartridge", "drawing", "vellum", "#fbfaf6", { depth: 0.3, absorption: 0.4, permeability: 0.3, roughness: 0.35 }),
168
+ preset("laid", "Laid paper", "drawing", "laid", "#f3ecdc", { depth: 0.45, absorption: 0.45, permeability: 0.35, roughness: 0.45 }),
169
+ preset("ingres", "Charcoal paper", "drawing", "ingres", "#ede6d6", { depth: 0.6, absorption: 0.4, permeability: 0.3, roughness: 0.65 }),
170
+ preset("sanded-pastel", "Sanded pastel paper", "pastel", "sanded", "#d8cfc0", { depth: 0.7, absorption: 0.3, permeability: 0.2, roughness: 0.9, shading: 0.35 }),
171
+ 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 }),
172
+ preset("cotton-canvas", "Cotton canvas", "canvas", "canvas", "#efe9dc", { depth: 0.55, absorption: 0.35, permeability: 0.25, roughness: 0.6 }),
173
+ preset("fine-linen", "Fine linen", "canvas", "linen", "#e9e2d2", { depth: 0.45, absorption: 0.3, permeability: 0.25, roughness: 0.5 }),
174
+ preset("gesso", "Gesso panel", "panel", "gesso", "#f6f4ef", { depth: 0.35, absorption: 0.2, permeability: 0.15, roughness: 0.4 }),
175
+ preset("smooth", "Smooth bristol", "smooth", "smooth", "#ffffff", { depth: 0, absorption: 0.3, permeability: 0.2, roughness: 0.05, shading: 0 })
176
+ ];
177
+ var DEFAULT_PAPER = PAPER_LIBRARY[0];
178
+ function paperPreset(id) {
179
+ const p = PAPER_LIBRARY.find((x) => x.preset === id);
180
+ return p ? clonePaper(p) : void 0;
181
+ }
182
+ function clonePaper(p) {
183
+ return { ...p, dry: { ...p.dry }, wet: { ...p.wet } };
184
+ }
185
+ function readTuning(raw, d) {
186
+ const r = raw && typeof raw === "object" ? raw : {};
187
+ return { contrast: clampNum(r.contrast, 0, 3, d.contrast), invert: typeof r.invert === "boolean" ? r.invert : d.invert };
188
+ }
189
+ function normalizePaper(raw = {}, base) {
190
+ const r = raw;
191
+ const b = _nullishCoalesce(_nullishCoalesce(base, () => ( (typeof r.preset === "string" ? paperPreset(r.preset) : void 0))), () => ( clonePaper(DEFAULT_PAPER)));
192
+ return {
193
+ preset: typeof r.preset === "string" && r.preset ? r.preset : b.preset,
194
+ name: typeof r.name === "string" && r.name.trim() ? r.name.trim() : b.name,
195
+ family: PAPER_FAMILIES.includes(r.family) ? r.family : b.family,
196
+ pattern: GRAIN_PATTERNS.includes(r.pattern) ? r.pattern : b.pattern,
197
+ seed: typeof r.seed === "number" && Number.isFinite(r.seed) ? Math.round(Math.abs(r.seed)) % 2147483647 : b.seed,
198
+ color: typeof r.color === "string" && HEX.test(r.color) ? r.color.toLowerCase() : b.color,
199
+ scale: clampNum(r.scale, 0.25, 8, b.scale),
200
+ tiling: r.tiling === "repeat" || r.tiling === "mirror" ? r.tiling : b.tiling,
201
+ depth: unit(r.depth, b.depth),
202
+ shading: unit(r.shading, b.shading),
203
+ dry: readTuning(r.dry, b.dry),
204
+ wet: readTuning(r.wet, b.wet),
205
+ absorption: unit(r.absorption, b.absorption),
206
+ permeability: unit(r.permeability, b.permeability),
207
+ roughness: unit(r.roughness, b.roughness)
208
+ };
209
+ }
210
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
211
+ function patternHeights(pattern, n, seed) {
212
+ const out = new Float32Array(n * n);
213
+ const salt = Math.imul(seed | 0, 2654435761) + 17;
214
+ if (pattern === "smooth") return out.fill(1);
215
+ const noise = (cx, cy, oct, s = 0) => new TileableNoise(n, cx, cy, oct, salt + s);
216
+ const k = n / GRAIN_TILE;
217
+ switch (pattern) {
218
+ case "cold-pressed": {
219
+ const base = noise(20 * k, 20 * k, 4);
220
+ const fine = noise(64 * k, 64 * k, 2, 3);
221
+ for (let y = 0; y < n; y++)
222
+ 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);
223
+ break;
224
+ }
225
+ case "hot-pressed": {
226
+ const base = noise(40 * k, 40 * k, 3);
227
+ 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);
228
+ break;
229
+ }
230
+ case "rough": {
231
+ const base = noise(10 * k, 10 * k, 5);
232
+ const blot = noise(28 * k, 28 * k, 2, 5);
233
+ for (let y = 0; y < n; y++)
234
+ 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);
235
+ break;
236
+ }
237
+ case "vellum": {
238
+ const base = noise(40 * k, 40 * k, 3);
239
+ 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);
240
+ break;
241
+ }
242
+ case "laid": {
243
+ const pulp = noise(32 * k, 32 * k, 3);
244
+ const chain = n / 8;
245
+ for (let y = 0; y < n; y++) {
246
+ const laid = 0.5 + 0.5 * Math.cos(2 * Math.PI * y / 4);
247
+ for (let x = 0; x < n; x++) {
248
+ const c = Math.abs((x + 0.5) % chain - chain / 2) / (chain / 2);
249
+ const chainLine = c > 0.94 ? -0.35 : 0;
250
+ out[y * n + x] = clamp01(0.3 + 0.5 * laid + chainLine + (pulp.at(x, y) - 0.5) * 0.6);
251
+ }
252
+ }
253
+ break;
254
+ }
255
+ case "ingres": {
256
+ const fibres = noise(64 * k, 12 * k, 3);
257
+ const base = noise(16 * k, 16 * k, 3, 7);
258
+ for (let y = 0; y < n; y++)
259
+ for (let x = 0; x < n; x++) {
260
+ const g = hash2(x, y, salt ^ 85);
261
+ 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));
262
+ }
263
+ break;
264
+ }
265
+ case "sanded": {
266
+ const grit = noise(96 * k, 96 * k, 2);
267
+ for (let y = 0; y < n; y++)
268
+ for (let x = 0; x < n; x++) {
269
+ const h = hash2(x, y, salt);
270
+ out[y * n + x] = clamp01((grit.at(x, y) - 0.5) * 2.4 + 0.5 + (h - 0.5) * 0.7);
271
+ }
272
+ break;
273
+ }
274
+ case "canvas":
275
+ case "linen": {
276
+ const period = (pattern === "canvas" ? 8 : 5) * k;
277
+ const threads = Math.max(2, 2 * Math.round(n / period / 2));
278
+ const p = n / threads;
279
+ const slub = noise(threads, threads, 2, 11);
280
+ for (let y = 0; y < n; y++)
281
+ for (let x = 0; x < n; x++) {
282
+ const fx = (x + 0.5) / p;
283
+ const fy = (y + 0.5) / p;
284
+ const cx = Math.floor(fx);
285
+ const cy = Math.floor(fy);
286
+ const warp = Math.sin(Math.PI * (fx - cx));
287
+ const weft = Math.sin(Math.PI * (fy - cy));
288
+ const top = (cx + cy) % 2 === 0 ? warp : weft;
289
+ const irregular = pattern === "linen" ? (slub.at(x, y) - 0.5) * 0.5 : 0;
290
+ out[y * n + x] = clamp01(0.2 + 0.7 * top + irregular + (hash2(x, y, salt) - 0.5) * 0.15);
291
+ }
292
+ break;
293
+ }
294
+ case "gesso": {
295
+ const streaks = noise(2 * k, 28 * k, 3);
296
+ const tooth = noise(64 * k, 64 * k, 2, 13);
297
+ for (let y = 0; y < n; y++)
298
+ 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);
299
+ break;
300
+ }
301
+ }
302
+ return out;
303
+ }
304
+ var rawCache = /* @__PURE__ */ new Map();
305
+ function grainHeights(pattern, seed, size = GRAIN_TILE) {
306
+ const key = `${pattern}:${seed}:${size}`;
307
+ let h = rawCache.get(key);
308
+ if (!h) {
309
+ h = patternHeights(pattern, size, seed);
310
+ if (rawCache.size > 32) rawCache.delete(rawCache.keys().next().value);
311
+ rawCache.set(key, h);
312
+ }
313
+ return h;
314
+ }
315
+ var PaperGrain = (_class2 = class {
316
+ __init2() {this.size = GRAIN_TILE}
317
+
318
+
319
+
320
+
321
+ constructor(paper, media = "dry") {;_class2.prototype.__init2.call(this);
322
+ const raw = grainHeights(paper.pattern, paper.seed);
323
+ const t = paper[media];
324
+ this.flat = paper.pattern === "smooth";
325
+ if (t.contrast === 1 && !t.invert) this.heights = raw;
326
+ else {
327
+ const out = new Float32Array(raw.length);
328
+ for (let i = 0; i < raw.length; i++) {
329
+ const v = clamp01((raw[i] - 0.5) * t.contrast + 0.5);
330
+ out[i] = t.invert ? 1 - v : v;
331
+ }
332
+ this.heights = out;
333
+ }
334
+ this.scale = paper.scale;
335
+ this.mirror = paper.tiling === "mirror";
336
+ }
337
+ /** Tile cell index for a coordinate in grain pixels (mirrored on odd tiles when mirroring). */
338
+ wrap(v) {
339
+ const n = this.size;
340
+ if (!this.mirror) return (v % n + n) % n;
341
+ const p = (v % (2 * n) + 2 * n) % (2 * n);
342
+ return p < n ? p : 2 * n - 1 - p;
343
+ }
344
+ /** Height 0..1 at document position (x, y). */
345
+ at(x, y) {
346
+ const u = x / this.scale - 0.5;
347
+ const v = y / this.scale - 0.5;
348
+ const x0 = Math.floor(u);
349
+ const y0 = Math.floor(v);
350
+ const fx = u - x0;
351
+ const fy = v - y0;
352
+ const n = this.size;
353
+ const h = this.heights;
354
+ const a = this.wrap(x0);
355
+ const b = this.wrap(x0 + 1);
356
+ const c = this.wrap(y0) * n;
357
+ const d = this.wrap(y0 + 1) * n;
358
+ const top = h[c + a] + (h[c + b] - h[c + a]) * fx;
359
+ const bot = h[d + a] + (h[d + b] - h[d + a]) * fx;
360
+ return top + (bot - top) * fy;
361
+ }
362
+ }, _class2);
363
+ function grainCatch(h, depth, pressure) {
364
+ if (depth <= 0) return 1;
365
+ const t = depth * (0.75 - 0.65 * Math.min(1, Math.max(0, pressure)));
366
+ const u = clamp01((h - t + 0.18) / 0.36);
367
+ return 1 - depth + depth * u * u * (3 - 2 * u);
368
+ }
369
+ function hexToRgb8(hex) {
370
+ const n = parseInt(hex.slice(1, 7), 16);
371
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
372
+ }
373
+ function renderPaper(paper, rect, grain = new PaperGrain(paper)) {
374
+ const [r, g, b] = hexToRgb8(paper.color);
375
+ const out = new Uint8ClampedArray(rect.width * rect.height * 4);
376
+ const shade = grain.flat ? 0 : paper.shading * 0.16;
377
+ for (let y = 0; y < rect.height; y++) {
378
+ for (let x = 0; x < rect.width; x++) {
379
+ const i = (y * rect.width + x) * 4;
380
+ const k = shade ? 1 - shade * (1 - grain.at(rect.x + x + 0.5, rect.y + y + 0.5)) : 1;
381
+ out[i] = r * k;
382
+ out[i + 1] = g * k;
383
+ out[i + 2] = b * k;
384
+ out[i + 3] = 255;
385
+ }
386
+ }
387
+ return out;
388
+ }
389
+
390
+ // src/paint-editor/document.ts
391
+ var MIN_CANVAS_SIDE = 1;
392
+ var MAX_CANVAS_SIDE = 16384;
393
+ var DEFAULT_DPI = 300;
394
+ var nextId = 1;
395
+ function newPaintLayerId() {
396
+ return `pl${Date.now().toString(36)}${(nextId++).toString(36)}`;
397
+ }
398
+ function createPaintLayer(name, id = newPaintLayerId()) {
399
+ return { type: "pixel", id, name, visible: true, opacity: 1, blend: "normal", surface: _chunkCRMAXIUVcjs.emptySurface.call(void 0, 4), offset: { x: 0, y: 0 } };
400
+ }
401
+ var PaintDocumentError = class extends Error {
402
+ constructor(message) {
403
+ super(message);
404
+ this.name = "PaintDocumentError";
405
+ }
406
+ };
407
+ function normalizeCanvas(raw) {
408
+ const side = (v, what) => {
409
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new PaintDocumentError(`${what} must be a number of pixels (${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE}).`);
410
+ const n = Math.round(v);
411
+ 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.`);
412
+ return n;
413
+ };
414
+ const dpi = raw.dpi === void 0 ? DEFAULT_DPI : raw.dpi;
415
+ if (typeof dpi !== "number" || !Number.isFinite(dpi) || dpi < 1 || dpi > 1e4) throw new PaintDocumentError(`dpi ${dpi} is out of range; use 1..10000.`);
416
+ return { width: side(raw.width, "width"), height: side(raw.height, "height"), dpi: Math.round(dpi * 100) / 100 };
417
+ }
418
+ function createPaintDocument(options = {}) {
419
+ const canvas = normalizeCanvas({ width: _nullishCoalesce(options.width, () => ( 2048)), height: _nullishCoalesce(options.height, () => ( 1536)), dpi: options.dpi });
420
+ const layer = createPaintLayer("Layer 1");
421
+ return { canvas, paper: normalizePaper(_nullishCoalesce(options.paper, () => ( {}))), layers: [layer], activeLayerId: layer.id, strokes: [] };
422
+ }
423
+
424
+ // src/paint-editor/brushes.ts
425
+ var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer"];
426
+ var PAINT_BRUSH_CATEGORIES = ["pencil", "pastel", "charcoal", "airbrush", "marker", "ink", "eraser", "blender", "smudge", "mixer", "custom"];
427
+ var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
428
+ var DEFAULT_DYNAMICS = {
429
+ minSize: 0,
430
+ pressureFlow: 0,
431
+ tiltSize: 0,
432
+ tiltRoundness: 0,
433
+ tiltOpacity: 0,
434
+ velocitySize: 0,
435
+ velocityOpacity: 0,
436
+ sizeJitter: 0,
437
+ angleJitter: 0,
438
+ opacityJitter: 0,
439
+ scatter: 0
440
+ };
441
+ var unit2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
442
+ var range = (v, lo, hi, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d;
443
+ function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
444
+ return {
445
+ minSize: unit2(raw.minSize, base.minSize),
446
+ pressureFlow: unit2(raw.pressureFlow, base.pressureFlow),
447
+ tiltSize: unit2(raw.tiltSize, base.tiltSize),
448
+ tiltRoundness: unit2(raw.tiltRoundness, base.tiltRoundness),
449
+ tiltOpacity: unit2(raw.tiltOpacity, base.tiltOpacity),
450
+ velocitySize: range(raw.velocitySize, -1, 1, base.velocitySize),
451
+ velocityOpacity: range(raw.velocityOpacity, -1, 1, base.velocityOpacity),
452
+ sizeJitter: unit2(raw.sizeJitter, base.sizeJitter),
453
+ angleJitter: unit2(raw.angleJitter, base.angleJitter),
454
+ opacityJitter: unit2(raw.opacityJitter, base.opacityJitter),
455
+ scatter: range(raw.scatter, 0, 4, base.scatter)
456
+ };
457
+ }
458
+ var BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
459
+ function normalizePaintBrush(raw = {}, base) {
460
+ const b = _nullishCoalesce(base, () => ( BASE_BRUSH));
461
+ const id = typeof raw.id === "string" && BRUSH_ID.test(raw.id) ? raw.id : b.id;
462
+ return {
463
+ id,
464
+ name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
465
+ category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
466
+ engine: PAINT_BRUSH_ENGINES.includes(raw.engine) ? raw.engine : b.engine,
467
+ tip: _chunkPV42JITTcjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(raw.tip, () => ( {})) }),
468
+ grain: unit2(raw.grain, b.grain),
469
+ pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
470
+ rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
471
+ dynamics: normalizeDynamics(_nullishCoalesce(raw.dynamics, () => ( {})), b.dynamics),
472
+ mixer: _chunkPV42JITTcjs.normalizeMixer.call(void 0, { ...b.mixer, ..._nullishCoalesce(raw.mixer, () => ( {})) }),
473
+ loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
474
+ ...raw.builtin ? { builtin: true } : {}
475
+ };
476
+ }
477
+ var BASE_BRUSH = {
478
+ id: "round",
479
+ name: "Round",
480
+ category: "custom",
481
+ engine: "dab",
482
+ tip: { ..._chunkPV42JITTcjs.DEFAULT_BRUSH },
483
+ grain: 0,
484
+ pressureSmoothing: 0,
485
+ rotation: "fixed",
486
+ dynamics: { ...DEFAULT_DYNAMICS },
487
+ mixer: { ..._chunkPV42JITTcjs.DEFAULT_MIXER },
488
+ loadsColor: true
489
+ };
490
+ var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
491
+ var BUILTIN_BRUSHES = [
492
+ builtin({
493
+ id: "pencil-hb",
494
+ name: "HB pencil",
495
+ category: "pencil",
496
+ tip: { size: 4, hardness: 0.7, spacing: 0.08, opacity: 0.85, flow: 0.55, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
497
+ grain: 0.85,
498
+ pressureSmoothing: 0.3,
499
+ dynamics: { minSize: 0.55, tiltSize: 0.9, tiltRoundness: 0.7, tiltOpacity: 0.5 }
500
+ }),
501
+ builtin({
502
+ id: "pencil-6b",
503
+ name: "6B pencil",
504
+ category: "pencil",
505
+ tip: { size: 7, hardness: 0.55, spacing: 0.07, opacity: 1, flow: 0.75, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
506
+ grain: 0.7,
507
+ pressureSmoothing: 0.3,
508
+ dynamics: { minSize: 0.5, tiltSize: 1, tiltRoundness: 0.75, tiltOpacity: 0.35 }
509
+ }),
510
+ builtin({
511
+ id: "soft-pastel",
512
+ name: "Soft pastel",
513
+ category: "pastel",
514
+ rotation: "stroke",
515
+ 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 },
516
+ grain: 1,
517
+ pressureSmoothing: 0.25,
518
+ dynamics: { sizeJitter: 0.06, angleJitter: 0.02 }
519
+ }),
520
+ builtin({
521
+ id: "oil-pastel",
522
+ name: "Oil pastel",
523
+ category: "pastel",
524
+ rotation: "stroke",
525
+ 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" },
526
+ grain: 0.5,
527
+ pressureSmoothing: 0.25,
528
+ dynamics: { minSize: 0.6 }
529
+ }),
530
+ builtin({
531
+ id: "charcoal-stick",
532
+ name: "Charcoal stick",
533
+ category: "charcoal",
534
+ rotation: "stroke",
535
+ 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 },
536
+ grain: 0.95,
537
+ pressureSmoothing: 0.2,
538
+ dynamics: { sizeJitter: 0.1, angleJitter: 0.03, tiltSize: 0.6 }
539
+ }),
540
+ builtin({
541
+ id: "vine-charcoal",
542
+ name: "Vine charcoal",
543
+ category: "charcoal",
544
+ tip: { size: 10, hardness: 0.3, spacing: 0.06, opacity: 0.75, flow: 0.45, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
545
+ grain: 0.9,
546
+ pressureSmoothing: 0.2,
547
+ dynamics: { minSize: 0.4, tiltSize: 0.8, tiltRoundness: 0.5, tiltOpacity: 0.3, sizeJitter: 0.08 }
548
+ }),
549
+ builtin({
550
+ id: "airbrush",
551
+ name: "Airbrush",
552
+ category: "airbrush",
553
+ tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
554
+ grain: 0,
555
+ pressureSmoothing: 0.3,
556
+ dynamics: { pressureFlow: 1, tiltSize: 0.5, tiltRoundness: 0.6 }
557
+ }),
558
+ builtin({
559
+ id: "marker",
560
+ name: "Chisel marker",
561
+ category: "marker",
562
+ 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" },
563
+ grain: 0.1
564
+ }),
565
+ builtin({
566
+ id: "ink-pen",
567
+ name: "Ink pen",
568
+ category: "ink",
569
+ tip: { size: 4, hardness: 1, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
570
+ pressureSmoothing: 0.4,
571
+ dynamics: { minSize: 0.25, velocitySize: -0.45 }
572
+ }),
573
+ builtin({
574
+ id: "brush-pen",
575
+ name: "Brush pen",
576
+ category: "ink",
577
+ rotation: "stroke",
578
+ tip: { size: 14, hardness: 0.9, spacing: 0.04, roundness: 0.8, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
579
+ pressureSmoothing: 0.45,
580
+ dynamics: { minSize: 0.05, velocitySize: -0.3 }
581
+ }),
582
+ builtin({
583
+ id: "eraser-soft",
584
+ name: "Soft eraser",
585
+ category: "eraser",
586
+ engine: "eraser",
587
+ tip: { size: 40, hardness: 0.2, spacing: 0.08, opacity: 1, flow: 0.6, pressureSize: false, pressureOpacity: true }
588
+ }),
589
+ builtin({
590
+ id: "eraser-hard",
591
+ name: "Hard eraser",
592
+ category: "eraser",
593
+ engine: "eraser",
594
+ tip: { size: 16, hardness: 0.9, spacing: 0.06, opacity: 1, flow: 1, pressureSize: true, pressureOpacity: false },
595
+ grain: 0.3,
596
+ dynamics: { minSize: 0.5 }
597
+ }),
598
+ builtin({
599
+ id: "blender",
600
+ name: "Blender",
601
+ category: "blender",
602
+ engine: "mixer",
603
+ loadsColor: false,
604
+ tip: { size: 34, hardness: 0.4, spacing: 0.08, opacity: 1, flow: 0.35, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
605
+ mixer: { wet: 0.7, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: "pigment" }
606
+ }),
607
+ builtin({
608
+ id: "smudge",
609
+ name: "Smudge",
610
+ category: "smudge",
611
+ engine: "mixer",
612
+ loadsColor: false,
613
+ tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow: 0.8, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
614
+ mixer: { wet: 1, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: "rgb" }
615
+ }),
616
+ builtin({
617
+ id: "mixer-brush",
618
+ name: "Mixer Brush",
619
+ category: "mixer",
620
+ engine: "mixer",
621
+ tip: { size: 30, hardness: 0.7, spacing: 0.1, opacity: 1, flow: 0.5, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
622
+ mixer: { wet: 0.5, load: 0.95, mix: 0.5, loadEachStroke: true, cleanEachStroke: true, mixing: "pigment" },
623
+ dynamics: { minSize: 0.5 }
624
+ })
625
+ ];
626
+ var DEFAULT_BRUSH_ID = "pencil-6b";
627
+ function brushToJSON(brush) {
628
+ const { builtin: _b, ...rest } = brush;
629
+ return JSON.parse(JSON.stringify(rest));
630
+ }
631
+ function parseBrushLibrary(raw, problems = []) {
632
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.brushes) ? raw.brushes : null;
633
+ if (!list) throw new Error('Not a brush library: expected {"format":"pixerate-brushes","version":1,"brushes":[...]} or an array of brushes.');
634
+ const out = [];
635
+ list.forEach((item, i) => {
636
+ if (!item || typeof item !== "object") {
637
+ problems.push(`brushes[${i}] is not an object; skipped.`);
638
+ return;
639
+ }
640
+ const r = item;
641
+ if (typeof r.id !== "string" || !BRUSH_ID.test(r.id)) {
642
+ problems.push(`brushes[${i}] has no valid id (lowercase letters, digits and dashes); skipped.`);
643
+ return;
644
+ }
645
+ if (typeof r.name !== "string" || !r.name.trim()) {
646
+ problems.push(`brushes[${i}] ("${r.id}") has no name; skipped.`);
647
+ return;
648
+ }
649
+ const { builtin: _b, ...rest } = r;
650
+ out.push(normalizePaintBrush(rest));
651
+ });
652
+ return out;
653
+ }
654
+
655
+ // src/paint-editor/engine.ts
656
+ function penOrientation(p) {
657
+ if (typeof p.altitude === "number" && Number.isFinite(p.altitude)) {
658
+ const altitude = Math.min(Math.PI / 2, Math.max(0, p.altitude));
659
+ const azimuth = typeof p.azimuth === "number" && Number.isFinite(p.azimuth) ? p.azimuth : 0;
660
+ return { altitude, azimuth };
661
+ }
662
+ const hasX = typeof p.tiltX === "number" && Number.isFinite(p.tiltX);
663
+ const hasY = typeof p.tiltY === "number" && Number.isFinite(p.tiltY);
664
+ if (!hasX && !hasY) return null;
665
+ return tiltToOrientation(hasX ? p.tiltX : 0, hasY ? p.tiltY : 0);
666
+ }
667
+ function tiltToOrientation(tiltX, tiltY) {
668
+ const clampT = (v) => Math.min(89.999, Math.max(-89.999, v));
669
+ const tx = Math.tan(clampT(tiltX) * Math.PI / 180);
670
+ const ty = Math.tan(clampT(tiltY) * Math.PI / 180);
671
+ const r = Math.hypot(tx, ty);
672
+ if (r < 1e-9) return { altitude: Math.PI / 2, azimuth: 0 };
673
+ let azimuth = Math.atan2(ty, tx);
674
+ if (azimuth < 0) azimuth += 2 * Math.PI;
675
+ return { altitude: Math.atan(1 / r), azimuth };
676
+ }
677
+ var pressureOf = (p) => typeof p.pressure === "number" && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1;
678
+ var PenSmoother = (_class3 = class {
679
+ __init3() {this.pos = null}
680
+ __init4() {this.pressure = null}
681
+
682
+
683
+ constructor(position, pressure) {;_class3.prototype.__init3.call(this);_class3.prototype.__init4.call(this);
684
+ this.a = 1 - Math.min(0.95, Math.max(0, position));
685
+ this.ap = 1 - Math.min(0.95, Math.max(0, pressure));
686
+ }
687
+ push(p) {
688
+ const pr = pressureOf(p);
689
+ if (!this.pos) {
690
+ this.pos = { x: p.x, y: p.y };
691
+ this.pressure = pr;
692
+ return p;
693
+ }
694
+ this.pos = { x: this.pos.x + (p.x - this.pos.x) * this.a, y: this.pos.y + (p.y - this.pos.y) * this.a };
695
+ this.pressure = this.pressure + (pr - this.pressure) * this.ap;
696
+ return { ...p, x: this.pos.x, y: this.pos.y, pressure: this.pressure };
697
+ }
698
+ /** Where the smoothed position is now (or `null` before the first sample). */
699
+ get position() {
700
+ return this.pos;
701
+ }
702
+ }, _class3);
703
+ var IDENTITY_COPY = { map: (p) => p, linear: [1, 0, 0, 1] };
704
+ var FULL_SPEED = 2.5;
705
+ var PenDabWalker = (_class4 = class {
706
+ constructor(brush, seed = 0, linear = [1, 0, 0, 1]) {;_class4.prototype.__init5.call(this);_class4.prototype.__init6.call(this);_class4.prototype.__init7.call(this);_class4.prototype.__init8.call(this);_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);
707
+ this.brush = brush;
708
+ this.seed = seed;
709
+ this.linear = linear;
710
+ }
711
+
712
+
713
+
714
+ __init5() {this.last = null}
715
+ __init6() {this.nextAt = 0}
716
+ __init7() {this.pendingFirst = null}
717
+ __init8() {this.index = 0}
718
+ __init9() {this.dir = 0}
719
+ __init10() {this.lastT = null}
720
+ __init11() {this.speed = null}
721
+ sample(p) {
722
+ const o = penOrientation(p);
723
+ let speed = null;
724
+ if (typeof p.t === "number" && Number.isFinite(p.t)) {
725
+ if (this.last && this.lastT !== null && p.t > this.lastT) {
726
+ const v = Math.hypot(p.x - this.last.x, p.y - this.last.y) / (p.t - this.lastT);
727
+ this.speed = this.speed === null ? v : this.speed + (v - this.speed) * 0.35;
728
+ }
729
+ this.lastT = p.t;
730
+ speed = _nullishCoalesce(this.speed, () => ( 0));
731
+ }
732
+ return {
733
+ x: p.x,
734
+ y: p.y,
735
+ pressure: pressureOf(p),
736
+ lean: o ? 1 - o.altitude / (Math.PI / 2) : 0,
737
+ azimuth: o ? o.azimuth : 0,
738
+ twist: typeof p.twist === "number" && Number.isFinite(p.twist) ? p.twist : null,
739
+ speed
740
+ };
741
+ }
742
+ diameter(s) {
743
+ const tip = this.brush.tip;
744
+ const dyn = this.brush.dynamics;
745
+ let d = tip.size;
746
+ if (tip.pressureSize) d *= dyn.minSize + (1 - dyn.minSize) * s.pressure;
747
+ d *= 1 + dyn.tiltSize * s.lean * 1.5;
748
+ 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));
749
+ return d;
750
+ }
751
+ stepLen(s) {
752
+ return Math.max(0.5, Math.max(1, this.diameter(s)) * Math.max(0.01, this.brush.tip.spacing));
753
+ }
754
+ /** A rasterizer angle (CCW, y up) mapped through this copy's linear part. */
755
+ mapAngle(theta) {
756
+ const [a, b, c, d] = this.linear;
757
+ if (a === 1 && b === 0 && c === 0 && d === 1) return theta;
758
+ const vx = Math.cos(theta);
759
+ const vy = -Math.sin(theta);
760
+ return Math.atan2(-(c * vx + d * vy), a * vx + b * vy);
761
+ }
762
+ dab(s) {
763
+ const { tip, dynamics: dyn, rotation } = this.brush;
764
+ const i = this.index++;
765
+ const seed = this.seed;
766
+ let diameter = this.diameter(s);
767
+ if (dyn.sizeJitter) diameter *= Math.max(0.05, 1 + dyn.sizeJitter * randSigned(seed, i, 1 /* Size */));
768
+ const base = tip.angle * Math.PI / 180;
769
+ let angle;
770
+ if (rotation === "stroke") angle = this.dir + base;
771
+ else if (rotation === "barrel" && s.twist !== null) angle = this.mapAngle(base - s.twist * Math.PI / 180);
772
+ else if (dyn.tiltRoundness > 0 && s.lean > 0.02) angle = this.mapAngle(base - s.azimuth);
773
+ else angle = this.mapAngle(base);
774
+ if (dyn.angleJitter) angle += dyn.angleJitter * Math.PI * randSigned(seed, i, 2 /* Angle */);
775
+ let opacity = tip.pressureOpacity ? s.pressure : 1;
776
+ opacity *= 1 - dyn.tiltOpacity * s.lean * 0.8;
777
+ if (s.speed !== null && dyn.velocityOpacity) opacity *= Math.max(0, 1 + dyn.velocityOpacity * (Math.min(1, s.speed / FULL_SPEED) - 0.5));
778
+ if (dyn.opacityJitter) opacity *= 1 - dyn.opacityJitter * (randSigned(seed, i, 3 /* Opacity */) * 0.5 + 0.5);
779
+ const flow = tip.flow * (1 - dyn.pressureFlow + dyn.pressureFlow * s.pressure);
780
+ let { x, y } = s;
781
+ if (dyn.scatter) {
782
+ const off = dyn.scatter * diameter * randSigned(seed, i, 4 /* Scatter */);
783
+ x += Math.sin(this.dir) * off;
784
+ y += Math.cos(this.dir) * off;
785
+ }
786
+ return {
787
+ x,
788
+ y,
789
+ radius: Math.max(0.5, diameter / 2),
790
+ alpha: Math.min(1, Math.max(0, flow)),
791
+ opacity: Math.min(1, Math.max(0, opacity)),
792
+ angle,
793
+ roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),
794
+ pressure: s.pressure
795
+ };
796
+ }
797
+ push(p, out) {
798
+ const a = this.last;
799
+ const s = this.sample(p);
800
+ if (!a) {
801
+ this.last = s;
802
+ this.pendingFirst = s;
803
+ this.nextAt = this.stepLen(s);
804
+ return;
805
+ }
806
+ const dx = s.x - a.x;
807
+ const dy = s.y - a.y;
808
+ const len = Math.hypot(dx, dy);
809
+ if (len > 1e-9) this.dir = Math.atan2(-dy, dx);
810
+ if (this.pendingFirst) {
811
+ out.push(this.dab(this.pendingFirst));
812
+ this.pendingFirst = null;
813
+ }
814
+ if (len > 1e-9) {
815
+ while (this.nextAt <= len + 1e-9) {
816
+ const f = Math.min(1, this.nextAt / len);
817
+ const lerp = (u, v) => u + (v - u) * f;
818
+ const q = {
819
+ x: a.x + dx * f,
820
+ y: a.y + dy * f,
821
+ pressure: lerp(a.pressure, s.pressure),
822
+ lean: lerp(a.lean, s.lean),
823
+ azimuth: lerpAngle(a.azimuth, s.azimuth, f),
824
+ twist: a.twist !== null && s.twist !== null ? lerpDegrees(a.twist, s.twist, f) : s.twist,
825
+ speed: a.speed !== null && s.speed !== null ? lerp(a.speed, s.speed) : s.speed
826
+ };
827
+ out.push(this.dab(q));
828
+ this.nextAt += this.stepLen(q);
829
+ }
830
+ this.nextAt -= len;
831
+ }
832
+ this.last = s;
833
+ }
834
+ finish(out) {
835
+ if (this.pendingFirst) {
836
+ out.push(this.dab(this.pendingFirst));
837
+ this.pendingFirst = null;
838
+ }
839
+ }
840
+ }, _class4);
841
+ function lerpAngle(a, b, t) {
842
+ let d = b - a;
843
+ while (d > Math.PI) d -= 2 * Math.PI;
844
+ while (d < -Math.PI) d += 2 * Math.PI;
845
+ return a + d * t;
846
+ }
847
+ function lerpDegrees(a, b, t) {
848
+ let d = b - a;
849
+ while (d > 180) d -= 360;
850
+ while (d < -180) d += 360;
851
+ return a + d * t;
852
+ }
853
+ var DryStroke = (_class5 = class {
854
+ constructor(brush, options) {;_class5.prototype.__init12.call(this);_class5.prototype.__init13.call(this);_class5.prototype.__init14.call(this);_class5.prototype.__init15.call(this);_class5.prototype.__init16.call(this);
855
+ this.brush = brush;
856
+ this.options = options;
857
+ this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
858
+ this.copies = _optionalChain([options, 'access', _ => _.copies, 'optionalAccess', _2 => _2.length]) ? options.copies : [IDENTITY_COPY];
859
+ this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, (_nullishCoalesce(options.seed, () => ( 0))) + i * 7919 | 0, c.linear));
860
+ this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
861
+ }
862
+
863
+
864
+ __init12() {this.coverage = new (0, _chunkPV42JITTcjs.CoverageMap)()}
865
+ __init13() {this.dabCount = 0}
866
+
867
+
868
+
869
+
870
+ __init14() {this.buf = new Float32Array(0)}
871
+ __init15() {this.lastRaw = null}
872
+ __init16() {this.lastFed = null}
873
+ /** Feeds raw pen samples; returns the document rectangles the new dabs touched. */
874
+ push(points) {
875
+ const dabs = [];
876
+ for (const raw of points) {
877
+ this.lastRaw = raw;
878
+ const p = this.smoother.push(raw);
879
+ this.lastFed = p;
880
+ this.copies.forEach((c, i) => this.walkers[i].push(c.map(p), dabs));
881
+ }
882
+ return this.paint(dabs);
883
+ }
884
+ /** Ends the stroke: the smoothed path catches up with the last raw sample; a single tap paints one dab. */
885
+ finish() {
886
+ const dabs = [];
887
+ const raw = this.lastRaw;
888
+ const fed = this.lastFed;
889
+ 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));
890
+ for (const w of this.walkers) w.finish(dabs);
891
+ return this.paint(dabs);
892
+ }
893
+ paint(dabs) {
894
+ const rects = [];
895
+ const hardness = this.brush.tip.hardness;
896
+ const grain = this.options.grain;
897
+ for (const d of dabs) {
898
+ const rect = _chunkPV42JITTcjs.dabRect.call(void 0, d, hardness);
899
+ const n = rect.width * rect.height;
900
+ if (this.buf.length < n) this.buf = new Float32Array(n);
901
+ const vals = this.buf.subarray(0, n);
902
+ _chunkPV42JITTcjs.rasterizeDab.call(void 0, d, hardness, rect, vals);
903
+ if (this.depth > 0 && grain) {
904
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
905
+ const y = rect.y + yy + 0.5;
906
+ for (let xx = 0; xx < rect.width; xx++, k++) {
907
+ if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
908
+ }
909
+ }
910
+ }
911
+ this.coverage.accumulate(rect, vals, d.opacity);
912
+ rects.push(rect);
913
+ this.dabCount++;
914
+ }
915
+ return rects;
916
+ }
917
+ }, _class5);
918
+ function mixerTip(brush) {
919
+ return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };
920
+ }
921
+ function toStrokePoint(p) {
922
+ return { x: p.x, y: p.y, pressure: pressureOf(p), ...typeof p.t === "number" ? { t: p.t } : {} };
923
+ }
924
+
925
+ // src/paint-editor/stroke.ts
926
+ function defaultTile(surface) {
927
+ const data = new Uint8ClampedArray(_chunkCRMAXIUVcjs.TILE * _chunkCRMAXIUVcjs.TILE * surface.channels);
928
+ if (surface.defaultValue) data.fill(surface.defaultValue);
929
+ return data;
930
+ }
931
+ function sameTile(a, b) {
932
+ if (!a || !b) return !a && !b;
933
+ if (a === b) return true;
934
+ if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;
935
+ for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;
936
+ return true;
937
+ }
938
+ function union(a, b) {
939
+ if (!a) return { ...b };
940
+ const x = Math.min(a.x, b.x);
941
+ const y = Math.min(a.y, b.y);
942
+ 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 };
943
+ }
944
+ var PaintStrokeSession = (_class6 = class {
945
+ constructor(host, init) {;_class6.prototype.__init17.call(this);_class6.prototype.__init18.call(this);_class6.prototype.__init19.call(this);_class6.prototype.__init20.call(this);_class6.prototype.__init21.call(this);_class6.prototype.__init22.call(this);_class6.prototype.__init23.call(this);_class6.prototype.__init24.call(this);_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);
946
+ this.host = host;
947
+ this.init = init;
948
+ this.layerId = init.layerId;
949
+ this.base = init.surface;
950
+ this.live = new Map(this.base.tiles);
951
+ this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
952
+ const brush = init.brush;
953
+ this.apply = {
954
+ mode: brush.engine === "eraser" ? "erase" : "paint",
955
+ color: init.color,
956
+ background: init.background,
957
+ opacity: brush.tip.opacity,
958
+ blend: brush.tip.blend,
959
+ lockTransparency: init.lockTransparency,
960
+ selection: null,
961
+ offset: init.offset
962
+ };
963
+ if (brush.engine === "mixer") {
964
+ this.mixerSmoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
965
+ const load = init.mixerLoad;
966
+ for (let i = 0; i < this.copies.length; i++) this.mixers.push(new (0, _chunkPV42JITTcjs.MixerStroke)(mixerTip(brush), brush.mixer, load, this.mixerTarget()));
967
+ } else {
968
+ this.dry = new DryStroke(brush, { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies });
969
+ }
970
+ }
971
+
972
+
973
+
974
+ /** The raw input so far (what a replay feeds back in). */
975
+ __init17() {this.points = []}
976
+
977
+
978
+ __init18() {this.touched = /* @__PURE__ */ new Set()}
979
+ __init19() {this.fresh = /* @__PURE__ */ new Set()}
980
+ __init20() {this.dry = null}
981
+ __init21() {this.mixers = []}
982
+ __init22() {this.mixerSmoother = null}
983
+
984
+
985
+ __init23() {this.bounds = null}
986
+ __init24() {this.lastRaw = null}
987
+ __init25() {this.closed = false}
988
+ __init26() {this.result = null}
989
+ mixerTarget() {
990
+ return {
991
+ offset: this.init.offset,
992
+ lockTransparency: this.init.lockTransparency,
993
+ selection: null,
994
+ readTile: (tx, ty) => _nullishCoalesce(_optionalChain([this, 'access', _3 => _3.live, 'access', _4 => _4.get, 'call', _5 => _5(_chunkCRMAXIUVcjs.tileKey.call(void 0, tx, ty)), 'optionalAccess', _6 => _6.data]), () => ( null)),
995
+ writeTile: (tx, ty) => {
996
+ const key = _chunkCRMAXIUVcjs.tileKey.call(void 0, tx, ty);
997
+ if (!this.touched.has(key)) {
998
+ const src = _nullishCoalesce(_optionalChain([this, 'access', _7 => _7.base, 'access', _8 => _8.tiles, 'access', _9 => _9.get, 'call', _10 => _10(key), 'optionalAccess', _11 => _11.data]), () => ( defaultTile(this.base)));
999
+ this.live.set(key, { data: new Uint8ClampedArray(src), uniform: null });
1000
+ this.touched.add(key);
1001
+ }
1002
+ this.fresh.add(key);
1003
+ return this.live.get(key).data;
1004
+ }
1005
+ };
1006
+ }
1007
+ get open() {
1008
+ return !this.closed;
1009
+ }
1010
+ /** Adds raw pen samples (document pixels). */
1011
+ push(points) {
1012
+ if (this.closed || !points.length) return;
1013
+ for (const p of points) this.points.push(p);
1014
+ this.lastRaw = points[points.length - 1];
1015
+ if (this.dry) this.update(this.dry.push(points));
1016
+ else {
1017
+ const rects = [];
1018
+ for (const raw of points) {
1019
+ const p = toStrokePoint(this.mixerSmoother.push(raw));
1020
+ this.copies.forEach((c, i) => rects.push(...this.mixers[i].push([toStrokePoint(c.map(p))])));
1021
+ }
1022
+ this.update(rects);
1023
+ }
1024
+ }
1025
+ update(rects) {
1026
+ if (!rects.length) return;
1027
+ for (const r of rects) this.bounds = union(this.bounds, r);
1028
+ const ch = this.base.channels;
1029
+ if (!this.dry) {
1030
+ for (const key of this.fresh) this.live.set(key, { data: this.live.get(key).data, uniform: null });
1031
+ this.fresh.clear();
1032
+ this.host.update(this.layerId, new (0, _chunkCRMAXIUVcjs.TiledSurface)(ch, new Map(this.live), this.base.defaultValue), rects);
1033
+ return;
1034
+ }
1035
+ const off = this.init.offset;
1036
+ const regions = /* @__PURE__ */ new Map();
1037
+ for (const r of rects) {
1038
+ for (const t of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, { x: r.x - off.x, y: r.y - off.y, width: r.width, height: r.height })) {
1039
+ const key = _chunkCRMAXIUVcjs.tileKey.call(void 0, t.tx, t.ty);
1040
+ const cur = regions.get(key);
1041
+ regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? union(cur.r, r) : r });
1042
+ }
1043
+ }
1044
+ for (const [key, { tx, ty, r }] of regions) {
1045
+ const src = _nullishCoalesce(_optionalChain([this, 'access', _12 => _12.base, 'access', _13 => _13.tiles, 'access', _14 => _14.get, 'call', _15 => _15(key), 'optionalAccess', _16 => _16.data]), () => ( defaultTile(this.base)));
1046
+ const prev = this.touched.has(key) ? _optionalChain([this, 'access', _17 => _17.live, 'access', _18 => _18.get, 'call', _19 => _19(key), 'optionalAccess', _20 => _20.data]) : void 0;
1047
+ this.touched.add(key);
1048
+ const data = _chunkPV42JITTcjs.applyStrokeToTile.call(void 0, this.dry.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});
1049
+ this.live.set(key, { data, uniform: null });
1050
+ }
1051
+ this.host.update(this.layerId, new (0, _chunkCRMAXIUVcjs.TiledSurface)(ch, new Map(this.live), this.base.defaultValue), rects);
1052
+ }
1053
+ /** Ends the stroke (one undo step) and returns what changed. */
1054
+ end() {
1055
+ if (this.closed) throw new Error("This stroke has already ended.");
1056
+ if (this.dry) this.update(this.dry.finish());
1057
+ else {
1058
+ const rects = [];
1059
+ const last = this.lastRaw ? toStrokePoint(this.lastRaw) : void 0;
1060
+ this.copies.forEach((c, i) => rects.push(...this.mixers[i].finish(last ? toStrokePoint(c.map(last)) : void 0)));
1061
+ this.update(rects);
1062
+ }
1063
+ this.closed = true;
1064
+ const ch = this.base.channels;
1065
+ const tiles = new Map(this.base.tiles);
1066
+ let changed = false;
1067
+ for (const key of this.touched) {
1068
+ const tile = _chunkCRMAXIUVcjs.finishTile.call(void 0, this.live.get(key).data, ch, this.base.defaultValue);
1069
+ const prev = this.base.tiles.get(key);
1070
+ if (tile) tiles.set(key, tile);
1071
+ else tiles.delete(key);
1072
+ if (!sameTile(prev, tile)) changed = true;
1073
+ }
1074
+ const surface = changed ? new (0, _chunkCRMAXIUVcjs.TiledSurface)(ch, tiles, this.base.defaultValue) : null;
1075
+ const dabs = this.dry ? this.dry.dabCount : this.mixers.reduce((n, m) => n + m.dabCount, 0);
1076
+ this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };
1077
+ if (this.mixers.length) {
1078
+ const brush = this.init.brush;
1079
+ let load = this.mixers[0].load;
1080
+ if (brush.mixer.cleanEachStroke) load = _chunkPV42JITTcjs.cleanMixer.call(void 0, load);
1081
+ this.result.mixerLoad = load;
1082
+ }
1083
+ this.host.commit(this, surface, false);
1084
+ return this.result;
1085
+ }
1086
+ /** Abandons the stroke and restores the layer. */
1087
+ cancel() {
1088
+ if (this.closed) return;
1089
+ this.closed = true;
1090
+ this.host.commit(this, null, true);
1091
+ }
1092
+ }, _class6);
1093
+ function mixerLoadForStroke(brush, current, color) {
1094
+ if (!brush.loadsColor) return { reservoir: null, amount: 0, pickup: brush.mixer.cleanEachStroke ? null : current.pickup };
1095
+ if (brush.mixer.loadEachStroke || !current.reservoir) return _chunkPV42JITTcjs.loadMixer.call(void 0, current, color);
1096
+ return current;
1097
+ }
1098
+
1099
+ // src/paint-editor/strokes.ts
1100
+ var IDENTITY_AFFINE = [1, 0, 0, 1, 0, 0];
1101
+ function applyAffine(m, x, y) {
1102
+ return { x: m[0] * x + m[1] * y + m[4], y: m[2] * x + m[3] * y + m[5] };
1103
+ }
1104
+ function composeAffine(a, b) {
1105
+ return [
1106
+ a[0] * b[0] + a[1] * b[2],
1107
+ a[0] * b[1] + a[1] * b[3],
1108
+ a[2] * b[0] + a[3] * b[2],
1109
+ a[2] * b[1] + a[3] * b[3],
1110
+ a[0] * b[4] + a[1] * b[5] + a[4],
1111
+ a[2] * b[4] + a[3] * b[5] + a[5]
1112
+ ];
1113
+ }
1114
+ function invertAffine(m) {
1115
+ const det = m[0] * m[3] - m[1] * m[2];
1116
+ if (Math.abs(det) < 1e-12) throw new Error("The transform is not invertible (scale 0?).");
1117
+ const a = m[3] / det;
1118
+ const b = -m[1] / det;
1119
+ const c = -m[2] / det;
1120
+ const d = m[0] / det;
1121
+ return [a, b, c, d, -(a * m[4] + b * m[5]), -(c * m[4] + d * m[5])];
1122
+ }
1123
+ function mapPenPoint(m, p) {
1124
+ const q = applyAffine(m, p.x, p.y);
1125
+ return { ...p, x: q.x, y: q.y };
1126
+ }
1127
+ function affineCopy(m) {
1128
+ const linear = [m[0], m[1], m[2], m[3]];
1129
+ return { map: (p) => mapPenPoint(m, p), linear };
1130
+ }
1131
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
1132
+ function readPoint(raw) {
1133
+ if (!raw || typeof raw !== "object") return null;
1134
+ const r = raw;
1135
+ if (!finite(r.x) || !finite(r.y)) return null;
1136
+ const p = { x: r.x, y: r.y };
1137
+ for (const k of ["pressure", "t", "tiltX", "tiltY", "altitude", "azimuth", "twist"]) if (finite(r[k])) p[k] = r[k];
1138
+ return p;
1139
+ }
1140
+ function readAffine(raw) {
1141
+ return Array.isArray(raw) && raw.length === 6 && raw.every(finite) ? raw : null;
1142
+ }
1143
+ function normalizeBrushStroke(raw) {
1144
+ if (!raw || typeof raw !== "object") return null;
1145
+ const r = raw;
1146
+ if (typeof r.id !== "string" || typeof r.layerId !== "string" || !Array.isArray(r.points)) return null;
1147
+ const points = r.points.map(readPoint).filter((p) => p !== null);
1148
+ if (!points.length) return null;
1149
+ const out = {
1150
+ id: r.id,
1151
+ layerId: r.layerId,
1152
+ brush: normalizePaintBrush(_nullishCoalesce(r.brush, () => ( {}))),
1153
+ color: typeof r.color === "string" && /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : "#000000",
1154
+ points,
1155
+ seed: finite(r.seed) ? r.seed | 0 : 0
1156
+ };
1157
+ const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m) => m !== null) : [];
1158
+ if (copies.length) out.copies = copies;
1159
+ return out;
1160
+ }
1161
+ function strokeBounds(points) {
1162
+ let x0 = Infinity;
1163
+ let y0 = Infinity;
1164
+ let x1 = -Infinity;
1165
+ let y1 = -Infinity;
1166
+ for (const p of points) {
1167
+ if (p.x < x0) x0 = p.x;
1168
+ if (p.y < y0) y0 = p.y;
1169
+ if (p.x > x1) x1 = p.x;
1170
+ if (p.y > y1) y1 = p.y;
1171
+ }
1172
+ return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
1173
+ }
1174
+ function variationAffine(v, origin) {
1175
+ const s = _nullishCoalesce(v.scale, () => ( 1));
1176
+ const a = (_nullishCoalesce(v.rotate, () => ( 0))) * Math.PI / 180;
1177
+ const cos = Math.cos(a) * s;
1178
+ const sin = Math.sin(a) * s;
1179
+ const fx = v.flipX ? -1 : 1;
1180
+ const linear = [cos * fx, -sin, sin * fx, cos, 0, 0];
1181
+ const toOrigin = [1, 0, 0, 1, -origin.x, -origin.y];
1182
+ const back = [1, 0, 0, 1, origin.x + (_nullishCoalesce(v.dx, () => ( 0))), origin.y + (_nullishCoalesce(v.dy, () => ( 0)))];
1183
+ return composeAffine(back, composeAffine(linear, toOrigin));
1184
+ }
1185
+ function varyStroke(stroke, m, v) {
1186
+ const s = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
1187
+ const ps = _nullishCoalesce(v.pressureScale, () => ( 1));
1188
+ const points = stroke.points.map((p) => {
1189
+ const q = mapPenPoint(m, p);
1190
+ if (ps !== 1) q.pressure = Math.min(1, Math.max(0, (_nullishCoalesce(p.pressure, () => ( 1))) * ps));
1191
+ return q;
1192
+ });
1193
+ const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;
1194
+ const out = { ...stroke, brush, points };
1195
+ if (_optionalChain([stroke, 'access', _21 => _21.copies, 'optionalAccess', _22 => _22.length])) {
1196
+ const inv = invertAffine(m);
1197
+ out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));
1198
+ }
1199
+ return out;
1200
+ }
1201
+
1202
+ // src/paint-editor/render.ts
1203
+ var PAPER_NODE_ID = "__paper";
1204
+ var TRACING_NODE_ID = "__tracing";
1205
+ function paperKey(p) {
1206
+ return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry]);
1207
+ }
1208
+ var paperNodes = /* @__PURE__ */ new WeakMap();
1209
+ function paperNode(paper) {
1210
+ let n = paperNodes.get(paper);
1211
+ if (!n) {
1212
+ n = { type: "paper", id: PAPER_NODE_ID, name: "Paper", visible: true, opacity: 1, blend: "normal", paper };
1213
+ paperNodes.set(paper, n);
1214
+ }
1215
+ return n;
1216
+ }
1217
+ var grainCache = /* @__PURE__ */ new Map();
1218
+ var tileCache = /* @__PURE__ */ new Map();
1219
+ function paperGrain(paper) {
1220
+ const key = JSON.stringify([paper.pattern, paper.seed, paper.scale, paper.tiling, paper.dry]);
1221
+ let g = grainCache.get(key);
1222
+ if (!g) {
1223
+ g = new PaperGrain(paper, "dry");
1224
+ if (grainCache.size > 16) grainCache.delete(grainCache.keys().next().value);
1225
+ grainCache.set(key, g);
1226
+ }
1227
+ return g;
1228
+ }
1229
+ function paperTile(paper, r) {
1230
+ const key = `${paperKey(paper)}|${r.x},${r.y},${r.width},${r.height}`;
1231
+ let t = tileCache.get(key);
1232
+ if (!t) {
1233
+ t = renderPaper(paper, r, paperGrain(paper));
1234
+ if (tileCache.size > 512) tileCache.delete(tileCache.keys().next().value);
1235
+ tileCache.set(key, t);
1236
+ }
1237
+ return t;
1238
+ }
1239
+ var PROFILE = {
1240
+ contentTile(layer, r) {
1241
+ if (layer.type === "paper") return paperTile(layer.paper, r);
1242
+ if (layer.type === "image") {
1243
+ const { image } = layer;
1244
+ const out = new Uint8ClampedArray(r.width * r.height * 4);
1245
+ const x0 = Math.max(0, r.x);
1246
+ const y0 = Math.max(0, r.y);
1247
+ const x1 = Math.min(image.width, r.x + r.width);
1248
+ const y1 = Math.min(image.height, r.y + r.height);
1249
+ if (x1 <= x0 || y1 <= y0) return null;
1250
+ 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);
1251
+ return out;
1252
+ }
1253
+ return null;
1254
+ },
1255
+ layerKey(layer) {
1256
+ if (layer.type === "paper") return paperKey(layer.paper);
1257
+ if (layer.type === "image") return layer.version;
1258
+ return void 0;
1259
+ }
1260
+ };
1261
+ function createPaintCompositor() {
1262
+ return new (0, _chunkWSTEXEKDcjs.RasterCompositor)(PROFILE);
1263
+ }
1264
+ function downsampleRgba(data, width, height, tw, th) {
1265
+ tw = Math.max(1, Math.round(tw));
1266
+ th = Math.max(1, Math.round(th));
1267
+ if (tw >= width && th >= height) return Uint8ClampedArray.from(data);
1268
+ const out = new Uint8ClampedArray(tw * th * 4);
1269
+ const sx = width / tw;
1270
+ const sy = height / th;
1271
+ for (let y = 0; y < th; y++) {
1272
+ const y0 = y * sy;
1273
+ const y1 = y0 + sy;
1274
+ for (let x = 0; x < tw; x++) {
1275
+ const x0 = x * sx;
1276
+ const x1 = x0 + sx;
1277
+ let r = 0;
1278
+ let g = 0;
1279
+ let b = 0;
1280
+ let a = 0;
1281
+ let w = 0;
1282
+ for (let yy = Math.floor(y0); yy < Math.ceil(y1) && yy < height; yy++) {
1283
+ const wy = Math.min(y1, yy + 1) - Math.max(y0, yy);
1284
+ for (let xx = Math.floor(x0); xx < Math.ceil(x1) && xx < width; xx++) {
1285
+ const wgt = wy * (Math.min(x1, xx + 1) - Math.max(x0, xx));
1286
+ const i = (yy * width + xx) * 4;
1287
+ const al = data[i + 3] * wgt;
1288
+ r += data[i] * al;
1289
+ g += data[i + 1] * al;
1290
+ b += data[i + 2] * al;
1291
+ a += al;
1292
+ w += wgt;
1293
+ }
1294
+ }
1295
+ const o = (y * tw + x) * 4;
1296
+ if (a > 0) {
1297
+ out[o] = r / a;
1298
+ out[o + 1] = g / a;
1299
+ out[o + 2] = b / a;
1300
+ }
1301
+ out[o + 3] = w > 0 ? a / w : 0;
1302
+ }
1303
+ }
1304
+ return out;
1305
+ }
1306
+
1307
+ // src/paint-editor/serialize.ts
1308
+ var PAINT_FORMAT = "pixerate-paint";
1309
+ var PAINT_SCHEMA_VERSION = 1;
1310
+ var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes"]);
1311
+ function documentExtensions(raw) {
1312
+ const out = {};
1313
+ for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;
1314
+ return out;
1315
+ }
1316
+ function header(state, options) {
1317
+ const out = {
1318
+ ..._nullishCoalesce(options.extensions, () => ( {})),
1319
+ format: PAINT_FORMAT,
1320
+ version: PAINT_SCHEMA_VERSION,
1321
+ width: state.canvas.width,
1322
+ height: state.canvas.height,
1323
+ dpi: state.canvas.dpi,
1324
+ paper: { ...state.paper, dry: { ...state.paper.dry }, wet: { ...state.paper.wet } },
1325
+ activeLayerId: state.activeLayerId
1326
+ };
1327
+ if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));
1328
+ return out;
1329
+ }
1330
+ function serializePaintDocument(state, options = {}) {
1331
+ const { layers, assets } = _chunkVSMM7B3Pcjs.serializeLayers.call(void 0, state.layers);
1332
+ return { ...header(state, options), layers, assets };
1333
+ }
1334
+ async function serializePaintDocumentExternal(state, store, options = {}) {
1335
+ const { layers, assets } = await _chunkVSMM7B3Pcjs.serializeLayersExternal.call(void 0, state.layers, store);
1336
+ return { ...header(state, options), layers, assets };
1337
+ }
1338
+ function paintLayersOnly(layers, problems) {
1339
+ const out = [];
1340
+ for (const l of layers) {
1341
+ if (l.type === "pixel") {
1342
+ const { filters, source, ...rest } = l;
1343
+ if (_optionalChain([filters, 'optionalAccess', _23 => _23.length])) problems.push(`Layer "${l.name}": its filters are an image editor feature and were dropped.`);
1344
+ if (source) problems.push(`Layer "${l.name}" refers to an image editor source image; it loads empty.`);
1345
+ out.push(rest);
1346
+ } else if (l.type === "group") out.push({ ...l, children: paintLayersOnly(l.children, problems) });
1347
+ else problems.push(`Layer "${l.name}" is an image editor ${l.type} layer, which the paint editor does not have; it was left out.`);
1348
+ }
1349
+ return out;
1350
+ }
1351
+ function readStrokes(raw, problems) {
1352
+ if (!Array.isArray(raw)) return [];
1353
+ const out = [];
1354
+ raw.forEach((s, i) => {
1355
+ const stroke = normalizeBrushStroke(s);
1356
+ if (stroke) out.push(stroke);
1357
+ else problems.push(`strokes[${i}] is not a recorded stroke; skipped.`);
1358
+ });
1359
+ return out;
1360
+ }
1361
+ function deserializePaintDocument(raw) {
1362
+ if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
1363
+ const r = raw;
1364
+ if (r.format !== PAINT_FORMAT) throw new Error(`Not a paint document: format is ${JSON.stringify(r.format)}, expected "${PAINT_FORMAT}".`);
1365
+ if (typeof r.version !== "number" || r.version > PAINT_SCHEMA_VERSION) {
1366
+ throw new Error(`Paint document version ${JSON.stringify(r.version)} is newer than this editor reads (${PAINT_SCHEMA_VERSION}); update @pixerate/editor.`);
1367
+ }
1368
+ const problems = [];
1369
+ const canvas = normalizeCanvas({ width: r.width, height: r.height, dpi: _nullishCoalesce(r.dpi, () => ( void 0)) });
1370
+ const paper = normalizePaper(_nullishCoalesce(r.paper, () => ( {})));
1371
+ const assets = r.assets && typeof r.assets === "object" ? r.assets : {};
1372
+ const layers = paintLayersOnly(_chunkVSMM7B3Pcjs.deserializeLayers.call(void 0, r.layers, { assets, problems }), problems);
1373
+ const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
1374
+ const activeLayerId = wanted && _chunkWSTEXEKDcjs.findLayer.call(void 0, layers, wanted) ? wanted : _nullishCoalesce(_optionalChain([_chunkWSTEXEKDcjs.flattenLayers.call(void 0, layers), 'access', _24 => _24.filter, 'call', _25 => _25((l) => l.type === "pixel"), 'access', _26 => _26.pop, 'call', _27 => _27(), 'optionalAccess', _28 => _28.id]), () => ( null));
1375
+ return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), problems };
1376
+ }
1377
+ async function deserializePaintDocumentAsync(raw, store) {
1378
+ if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
1379
+ const r = raw;
1380
+ const problems = [];
1381
+ const assets = await _chunkVSMM7B3Pcjs.resolveExternalAssets.call(void 0, _nullishCoalesce(r.assets, () => ( {})), store, problems);
1382
+ const loaded = deserializePaintDocument({ ...r, assets });
1383
+ return { ...loaded, problems: [...problems, ...loaded.problems] };
1384
+ }
1385
+
1386
+ // src/paint-editor/view.ts
1387
+ var MIN_ZOOM = 0.02;
1388
+ var MAX_ZOOM = 64;
1389
+ var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 }, rotation: 0, mirror: false };
1390
+ function normalizeRotation(deg) {
1391
+ if (!Number.isFinite(deg)) return 0;
1392
+ let r = deg % 360;
1393
+ if (r > 180) r -= 360;
1394
+ if (r <= -180) r += 360;
1395
+ return Math.round(r * 1e3) / 1e3;
1396
+ }
1397
+ function normalizeView(raw, base = DEFAULT_VIEW) {
1398
+ const zoom = typeof raw.zoom === "number" && Number.isFinite(raw.zoom) ? Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, raw.zoom)) : base.zoom;
1399
+ const pan = raw.pan && Number.isFinite(raw.pan.x) && Number.isFinite(raw.pan.y) ? { x: raw.pan.x, y: raw.pan.y } : { ...base.pan };
1400
+ return {
1401
+ zoom,
1402
+ pan,
1403
+ rotation: typeof raw.rotation === "number" ? normalizeRotation(raw.rotation) : base.rotation,
1404
+ mirror: typeof raw.mirror === "boolean" ? raw.mirror : base.mirror
1405
+ };
1406
+ }
1407
+ function viewMatrix(view, canvas, viewport) {
1408
+ const a = view.rotation * Math.PI / 180;
1409
+ const z = view.zoom;
1410
+ const m = view.mirror ? -1 : 1;
1411
+ const cos = Math.cos(a) * z;
1412
+ const sin = Math.sin(a) * z;
1413
+ const cx = canvas.width / 2;
1414
+ const cy = canvas.height / 2;
1415
+ const sx = viewport.width / 2 + view.pan.x;
1416
+ const sy = viewport.height / 2 + view.pan.y;
1417
+ const A = cos * m;
1418
+ const B = -sin;
1419
+ const C = sin * m;
1420
+ const D = cos;
1421
+ return [A, B, C, D, sx - (A * cx + B * cy), sy - (C * cx + D * cy)];
1422
+ }
1423
+ function screenToDocument(view, canvas, viewport, x, y) {
1424
+ return applyAffine(invertAffine(viewMatrix(view, canvas, viewport)), x, y);
1425
+ }
1426
+ function documentToScreen(view, canvas, viewport, x, y) {
1427
+ return applyAffine(viewMatrix(view, canvas, viewport), x, y);
1428
+ }
1429
+ function viewCssTransform(view, canvas, viewport) {
1430
+ const m = viewMatrix(view, canvas, viewport);
1431
+ const f = (v) => Math.round(v * 1e4) / 1e4;
1432
+ return `matrix(${f(m[0])}, ${f(m[2])}, ${f(m[1])}, ${f(m[3])}, ${f(m[4])}, ${f(m[5])})`;
1433
+ }
1434
+ function fitZoom(canvas, viewport, margin = 24) {
1435
+ const w = Math.max(1, viewport.width - 2 * margin);
1436
+ const h = Math.max(1, viewport.height - 2 * margin);
1437
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min(w / canvas.width, h / canvas.height)));
1438
+ }
1439
+ function viewPenToDocument(view, p) {
1440
+ if (!view.rotation && !view.mirror) return p;
1441
+ const out = { ...p };
1442
+ const r = view.rotation * Math.PI / 180;
1443
+ if (typeof p.azimuth === "number") {
1444
+ let a = p.azimuth - r;
1445
+ if (view.mirror) a = Math.PI - a;
1446
+ out.azimuth = (a % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
1447
+ }
1448
+ if (typeof p.twist === "number") {
1449
+ let t = p.twist - view.rotation;
1450
+ if (view.mirror) t = -t;
1451
+ out.twist = (t % 360 + 360) % 360;
1452
+ }
1453
+ return out;
1454
+ }
1455
+
1456
+ // src/paint-editor/input.ts
1457
+ function samplePressure(e) {
1458
+ if (e.pointerType === "pen" && typeof e.pressure === "number" && e.pressure > 0) return Math.min(1, e.pressure);
1459
+ return 1;
1460
+ }
1461
+ function pointerToPenPoint(e, x, y, view) {
1462
+ const p = { x, y, pressure: samplePressure(e) };
1463
+ if (typeof e.timeStamp === "number" && Number.isFinite(e.timeStamp)) p.t = e.timeStamp;
1464
+ if (e.pointerType !== "pen") return p;
1465
+ const o = penOrientation({
1466
+ x,
1467
+ y,
1468
+ altitude: typeof e.altitudeAngle === "number" ? e.altitudeAngle : void 0,
1469
+ azimuth: typeof e.azimuthAngle === "number" ? e.azimuthAngle : void 0,
1470
+ tiltX: e.tiltX,
1471
+ tiltY: e.tiltY
1472
+ });
1473
+ if (o && o.altitude < Math.PI / 2 - 1e-6) {
1474
+ p.altitude = o.altitude;
1475
+ p.azimuth = o.azimuth;
1476
+ }
1477
+ if (typeof e.twist === "number" && Number.isFinite(e.twist) && e.twist !== 0) p.twist = e.twist;
1478
+ return view ? viewPenToDocument(view, p) : p;
1479
+ }
1480
+ function isPalmTouch(touch, pen, hand) {
1481
+ if (!pen) return false;
1482
+ const dx = touch.x - pen.x;
1483
+ const dy = touch.y - pen.y;
1484
+ const side = hand === "right" ? dx : -dx;
1485
+ return dy > -40 && side > -40 && Math.hypot(dx, dy) < 600;
1486
+ }
1487
+
1488
+ // src/paint-editor/encode.ts
1489
+ function defaultImageEncoder() {
1490
+ const g = globalThis;
1491
+ if (typeof g.OffscreenCanvas !== "function" || typeof g.ImageData !== "function") return null;
1492
+ const Canvas = g.OffscreenCanvas;
1493
+ const Data = g.ImageData;
1494
+ return async (image, format, quality) => {
1495
+ const canvas = new Canvas(image.width, image.height);
1496
+ const ctx = canvas.getContext("2d");
1497
+ if (!ctx) throw new Error("No 2D context to encode with.");
1498
+ ctx.putImageData(new Data(new Uint8ClampedArray(image.data), image.width, image.height), 0, 0);
1499
+ const blob = await canvas.convertToBlob({ type: format, quality });
1500
+ if (blob.type !== format) throw new Error(`This browser cannot encode ${format} (it produced ${blob.type || "nothing"}); export PNG instead.`);
1501
+ return new Uint8Array(await blob.arrayBuffer());
1502
+ };
1503
+ }
1504
+
1505
+ // src/paint-editor/thumbnails.ts
1506
+ function thumbnailStroke(width, height) {
1507
+ const pts = [];
1508
+ const n = 64;
1509
+ const mx = width * 0.1;
1510
+ const my = height * 0.5;
1511
+ for (let i = 0; i <= n; i++) {
1512
+ const t = i / n;
1513
+ const pressure = t < 0.5 ? 0.15 + 1.7 * t : 1 - 1.4 * (t - 0.5);
1514
+ pts.push({ x: mx + (width - 2 * mx) * t, y: my - Math.sin(t * Math.PI * 2) * height * 0.22, pressure, t: i * 12 });
1515
+ }
1516
+ return pts;
1517
+ }
1518
+ function paintTestStroke(surface, brush, paper, color, width, height) {
1519
+ const [r, g, b] = hexToRgb8(color);
1520
+ let result = surface;
1521
+ const session = new PaintStrokeSession(
1522
+ { update: () => {
1523
+ }, commit: (_s, s) => result = _nullishCoalesce(s, () => ( result)) },
1524
+ {
1525
+ layerId: "thumbnail",
1526
+ surface,
1527
+ offset: { x: 0, y: 0 },
1528
+ lockTransparency: false,
1529
+ brush,
1530
+ color: [r, g, b, 255],
1531
+ background: [...hexToRgb8(paper.color), 255],
1532
+ grain: paperGrain(paper),
1533
+ paperDepth: paper.depth,
1534
+ seed: 7,
1535
+ colorHex: color,
1536
+ affineCopies: [],
1537
+ copies: [IDENTITY_COPY],
1538
+ mixerLoad: brush.engine === "mixer" ? mixerLoadForStroke(brush, _chunkPV42JITTcjs.EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]) : _chunkPV42JITTcjs.EMPTY_MIXER_LOAD
1539
+ }
1540
+ );
1541
+ session.push(thumbnailStroke(width, height));
1542
+ session.end();
1543
+ return result;
1544
+ }
1545
+ function renderBrushThumbnail(brush, options) {
1546
+ const width = Math.max(16, Math.min(512, Math.round(_nullishCoalesce(options.width, () => ( 160)))));
1547
+ const height = Math.max(16, Math.min(512, Math.round(_nullishCoalesce(options.height, () => ( 48)))));
1548
+ const maxSize = height * 0.42;
1549
+ const b = brush.tip.size > maxSize ? normalizePaintBrush({ tip: { ...brush.tip, size: maxSize } }, brush) : brush;
1550
+ const rgba = [...hexToRgb8(options.color), 255];
1551
+ let surface = _chunkCRMAXIUVcjs.emptySurface.call(void 0, 4);
1552
+ if (b.engine === "eraser") {
1553
+ surface = _chunkCRMAXIUVcjs.fillRect.call(void 0, surface, { x: 0, y: 0, width, height }, rgba);
1554
+ } else if (b.engine === "mixer" && !b.loadsColor) {
1555
+ const half = Math.ceil(height / 2);
1556
+ surface = _chunkCRMAXIUVcjs.fillRect.call(void 0, surface, { x: 0, y: 0, width, height: half }, rgba);
1557
+ surface = _chunkCRMAXIUVcjs.fillRect.call(void 0, surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
1558
+ }
1559
+ surface = paintTestStroke(surface, b, options.paper, options.color, width, height);
1560
+ const layer = { type: "pixel", id: "thumbnail", name: "thumbnail", visible: true, opacity: 1, blend: "normal", surface, offset: { x: 0, y: 0 } };
1561
+ const data = createPaintCompositor().composite([paperNode(options.paper), layer], { x: 0, y: 0, width, height }, { width, height });
1562
+ return { width, height, data };
1563
+ }
1564
+
1565
+ // src/paint-editor/controller.ts
1566
+ var PaintEditorError = class extends Error {
1567
+ constructor(message) {
1568
+ super(message);
1569
+ this.name = "PaintEditorError";
1570
+ }
1571
+ };
1572
+ var HEX2 = /^#[0-9a-f]{6}$/i;
1573
+ function parseColor(value, what = "color") {
1574
+ if (typeof value !== "string" || !HEX2.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
1575
+ return value.trim().toLowerCase();
1576
+ }
1577
+ function rgbaOf(hex) {
1578
+ const [r, g, b] = hexToRgb8(hex);
1579
+ return [r, g, b, 255];
1580
+ }
1581
+ function rgbToHex(r, g, b) {
1582
+ return "#" + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("");
1583
+ }
1584
+ function unionRect(a, b) {
1585
+ if (!a) return b ? { ...b } : null;
1586
+ if (!b) return { ...a };
1587
+ const x = Math.min(a.x, b.x);
1588
+ const y = Math.min(a.y, b.y);
1589
+ 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 };
1590
+ }
1591
+ var isPixelLayer = (l) => _optionalChain([l, 'optionalAccess', _29 => _29.type]) === "pixel";
1592
+ var PaintEditorController = (_class7 = class {
1593
+
1594
+ __init27() {this.extensions = {}}
1595
+ __init28() {this.undoStack = []}
1596
+ __init29() {this.redoStack = []}
1597
+
1598
+
1599
+
1600
+
1601
+ __init30() {this.secondaryColor = "#ffffff"}
1602
+ __init31() {this.mixerLoad = _chunkPV42JITTcjs.EMPTY_MIXER_LOAD}
1603
+ __init32() {this.view = { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } }}
1604
+
1605
+
1606
+ __init33() {this.listeners = /* @__PURE__ */ new Set()}
1607
+ __init34() {this.version = 0}
1608
+ __init35() {this.cached = null}
1609
+ __init36() {this.session = null}
1610
+ __init37() {this.live = null}
1611
+ __init38() {this.liveLayers = null}
1612
+ __init39() {this.dirty = "all"}
1613
+ __init40() {this.seedCounter = 1}
1614
+ __init41() {this.strokeCounter = 1}
1615
+ __init42() {this.viewCompositor = createPaintCompositor()}
1616
+ __init43() {this.exportCompositor = createPaintCompositor()}
1617
+ constructor(options = {}) {;_class7.prototype.__init27.call(this);_class7.prototype.__init28.call(this);_class7.prototype.__init29.call(this);_class7.prototype.__init30.call(this);_class7.prototype.__init31.call(this);_class7.prototype.__init32.call(this);_class7.prototype.__init33.call(this);_class7.prototype.__init34.call(this);_class7.prototype.__init35.call(this);_class7.prototype.__init36.call(this);_class7.prototype.__init37.call(this);_class7.prototype.__init38.call(this);_class7.prototype.__init39.call(this);_class7.prototype.__init40.call(this);_class7.prototype.__init41.call(this);_class7.prototype.__init42.call(this);_class7.prototype.__init43.call(this);
1618
+ this.historyLimit = Math.max(1, _nullishCoalesce(options.historyLimit, () => ( 200)));
1619
+ this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });
1620
+ this.userBrushes = (_nullishCoalesce(options.brushes, () => ( []))).map((b) => normalizePaintBrush({ ...b, builtin: false }));
1621
+ const start = _nullishCoalesce(this.findBrush(_nullishCoalesce(options.brushId, () => ( DEFAULT_BRUSH_ID))), () => ( this.findBrush(DEFAULT_BRUSH_ID)));
1622
+ this.brush = normalizePaintBrush(start);
1623
+ this.color = options.color ? parseColor(options.color) : "#1f2937";
1624
+ this.hand = options.hand === "left" ? "left" : "right";
1625
+ this.encodeImage = options.encodeImage;
1626
+ }
1627
+ // --- State and events ----------------------------------------------------------
1628
+ subscribe(listener) {
1629
+ this.listeners.add(listener);
1630
+ return () => this.listeners.delete(listener);
1631
+ }
1632
+ emit(dirty = "all") {
1633
+ if (dirty === "all" || this.dirty === "all") this.dirty = "all";
1634
+ else this.dirty.push(...dirty);
1635
+ this.version++;
1636
+ this.cached = null;
1637
+ for (const l of [...this.listeners]) l();
1638
+ }
1639
+ /** A snapshot of the editor (the same object until something changes). */
1640
+ getState() {
1641
+ if (this.cached) return this.cached;
1642
+ const last = this.undoStack[this.undoStack.length - 1];
1643
+ const next = this.redoStack[this.redoStack.length - 1];
1644
+ this.cached = {
1645
+ document: this.doc,
1646
+ brush: this.brush,
1647
+ brushes: [...BUILTIN_BRUSHES, ...this.userBrushes],
1648
+ color: this.color,
1649
+ secondaryColor: this.secondaryColor,
1650
+ mixerLoad: this.mixerLoad,
1651
+ view: this.view,
1652
+ hand: this.hand,
1653
+ canUndo: this.undoStack.length > 0 && !this.session,
1654
+ canRedo: this.redoStack.length > 0 && !this.session,
1655
+ undoLabel: _nullishCoalesce(_optionalChain([last, 'optionalAccess', _30 => _30.label]), () => ( null)),
1656
+ redoLabel: _nullishCoalesce(_optionalChain([next, 'optionalAccess', _31 => _31.label]), () => ( null)),
1657
+ stroking: Boolean(this.session),
1658
+ version: this.version
1659
+ };
1660
+ return this.cached;
1661
+ }
1662
+ get document() {
1663
+ return this.doc;
1664
+ }
1665
+ /**
1666
+ * Where the rendered stack changed since the last call: rectangles (a stroke's dabs) or `'all'`.
1667
+ * The viewport passes it to `compositeTiles` as `dirty`.
1668
+ */
1669
+ takeDirty() {
1670
+ const d = this.dirty;
1671
+ this.dirty = [];
1672
+ return d;
1673
+ }
1674
+ // --- History -------------------------------------------------------------------
1675
+ requireIdle(what) {
1676
+ if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);
1677
+ }
1678
+ commit(label, next) {
1679
+ this.undoStack.push({ label, before: this.doc, after: next });
1680
+ if (this.undoStack.length > this.historyLimit) this.undoStack.shift();
1681
+ this.redoStack = [];
1682
+ this.doc = next;
1683
+ this.emit();
1684
+ }
1685
+ undo() {
1686
+ this.requireIdle("undoing");
1687
+ const e = this.undoStack.pop();
1688
+ if (!e) return null;
1689
+ this.redoStack.push(e);
1690
+ this.doc = e.before;
1691
+ this.emit();
1692
+ return e.label;
1693
+ }
1694
+ redo() {
1695
+ this.requireIdle("redoing");
1696
+ const e = this.redoStack.pop();
1697
+ if (!e) return null;
1698
+ this.undoStack.push(e);
1699
+ this.doc = e.after;
1700
+ this.emit();
1701
+ return e.label;
1702
+ }
1703
+ /** Labels of the undo steps, oldest first. */
1704
+ historyLabels() {
1705
+ return { undo: this.undoStack.map((e) => e.label), redo: this.redoStack.map((e) => e.label).reverse() };
1706
+ }
1707
+ // --- Canvas and paper ----------------------------------------------------------
1708
+ resolvePaper(paper) {
1709
+ if (typeof paper === "string") {
1710
+ const p = paperPreset(paper);
1711
+ if (!p) throw new PaintEditorError(`Unknown paper "${paper}". Call list_papers for the ids.`);
1712
+ return p;
1713
+ }
1714
+ return _nullishCoalesce(paper, () => ( {}));
1715
+ }
1716
+ /** Starts a new canvas: one empty layer over the paper (one undo step; undo brings the old one back). */
1717
+ newCanvas(options = {}) {
1718
+ this.requireIdle("starting a new canvas");
1719
+ const next = createPaintDocument({
1720
+ width: _nullishCoalesce(options.width, () => ( this.doc.canvas.width)),
1721
+ height: _nullishCoalesce(options.height, () => ( this.doc.canvas.height)),
1722
+ dpi: _nullishCoalesce(options.dpi, () => ( this.doc.canvas.dpi)),
1723
+ paper: options.paper === void 0 ? this.doc.paper : this.resolvePaper(options.paper)
1724
+ });
1725
+ this.extensions = {};
1726
+ this.commit("New canvas", next);
1727
+ return next;
1728
+ }
1729
+ /** Changes the canvas size (keeping the pixels where they are, anchored top-left) or resolution. */
1730
+ setCanvas(update) {
1731
+ this.requireIdle("changing the canvas");
1732
+ const canvas = normalizeCanvas({
1733
+ width: _nullishCoalesce(update.width, () => ( this.doc.canvas.width)),
1734
+ height: _nullishCoalesce(update.height, () => ( this.doc.canvas.height)),
1735
+ dpi: _nullishCoalesce(update.dpi, () => ( this.doc.canvas.dpi))
1736
+ });
1737
+ this.commit("Canvas size", { ...this.doc, canvas });
1738
+ return canvas;
1739
+ }
1740
+ /** Changes the paper: a library preset (`preset`), and / or any of its settings (one undo step). */
1741
+ setPaper(update) {
1742
+ this.requireIdle("changing the paper");
1743
+ let base = this.doc.paper;
1744
+ if (update.preset !== void 0 && update.preset !== this.doc.paper.preset) {
1745
+ const p = paperPreset(update.preset);
1746
+ if (!p) throw new PaintEditorError(`Unknown paper "${update.preset}". Call list_papers for the ids.`);
1747
+ base = p;
1748
+ }
1749
+ const { preset: _p, ...rest } = update;
1750
+ if (rest.color !== void 0) parseColor(rest.color, "paper color");
1751
+ const paper = normalizePaper({ ...rest, preset: base.preset }, base);
1752
+ const changedKeys = Object.keys(rest).filter((k) => k !== "name");
1753
+ if (changedKeys.length && update.preset === void 0) paper.preset = base.preset;
1754
+ this.commit("Paper", { ...this.doc, paper });
1755
+ return clonePaper(paper);
1756
+ }
1757
+ // --- Layers --------------------------------------------------------------------
1758
+ getLayer(id) {
1759
+ return _optionalChain([_chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, id), 'optionalAccess', _32 => _32.layer]);
1760
+ }
1761
+ /** The active pixel layer (or `undefined`). */
1762
+ getActiveLayer() {
1763
+ return this.doc.activeLayerId ? this.getLayer(this.doc.activeLayerId) : void 0;
1764
+ }
1765
+ /** Every layer, top first, as panels and tools describe them. */
1766
+ listLayers() {
1767
+ const out = [];
1768
+ const walk = (list, parentId, depth) => {
1769
+ for (let i = list.length - 1; i >= 0; i--) {
1770
+ const l = list[i];
1771
+ out.push(this.describeLayer(l, parentId, depth));
1772
+ if (l.type === "group") walk(l.children, l.id, depth + 1);
1773
+ }
1774
+ };
1775
+ walk(this.doc.layers, null, 0);
1776
+ return out;
1777
+ }
1778
+ describeLayer(l, parentId = _nullishCoalesce(_optionalChain([_chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, l.id), 'optionalAccess', _33 => _33.parent, 'optionalAccess', _34 => _34.id]), () => ( null)), depth = _nullishCoalesce(_optionalChain([_chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, l.id), 'optionalAccess', _35 => _35.path, 'access', _36 => _36.length]), () => ( 0))) {
1779
+ let bounds = null;
1780
+ if (l.type === "pixel") {
1781
+ const b = _chunkCRMAXIUVcjs.contentBounds.call(void 0, l.surface);
1782
+ bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;
1783
+ } else for (const c of _chunkWSTEXEKDcjs.flattenLayers.call(void 0, l.children)) if (c.type === "pixel") bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);
1784
+ return {
1785
+ id: l.id,
1786
+ name: l.name,
1787
+ type: l.type,
1788
+ visible: l.visible,
1789
+ opacity: l.opacity,
1790
+ blend: l.blend,
1791
+ clipped: Boolean(l.clipped),
1792
+ locks: { ..._nullishCoalesce(l.locks, () => ( {})) },
1793
+ active: l.id === this.doc.activeLayerId,
1794
+ bounds,
1795
+ parentId,
1796
+ depth
1797
+ };
1798
+ }
1799
+ requireLayer(id) {
1800
+ const l = this.getLayer(id);
1801
+ if (!l) throw new PaintEditorError(`Unknown layer "${id}". Call list_layers for the ids.`);
1802
+ return l;
1803
+ }
1804
+ /** The pixel layer a paint operation targets, checked for locks and visibility. */
1805
+ paintTarget(layerId, what) {
1806
+ const id = _nullishCoalesce(layerId, () => ( this.doc.activeLayerId));
1807
+ if (!id) throw new PaintEditorError(`There is no active layer to ${what}; add one with add_layer.`);
1808
+ const l = this.requireLayer(id);
1809
+ if (!isPixelLayer(l)) throw new PaintEditorError(`Layer "${l.name}" is a group; ${what} a pixel layer inside it.`);
1810
+ if (_chunkWSTEXEKDcjs.isLayerLocked.call(void 0, l, "pixels")) throw new PaintEditorError(`Layer "${l.name}" has its pixels locked; unlock it (update_layer locks) before you ${what} it.`);
1811
+ if (!l.visible) throw new PaintEditorError(`Layer "${l.name}" is hidden; show it (update_layer visible true) before you ${what} it.`);
1812
+ return l;
1813
+ }
1814
+ setActiveLayer(id) {
1815
+ this.requireLayer(id);
1816
+ if (this.doc.activeLayerId === id) return;
1817
+ this.doc = { ...this.doc, activeLayerId: id };
1818
+ this.emit();
1819
+ }
1820
+ /** Adds an empty pixel layer or group above `aboveId` (default: the active layer) and makes it active. */
1821
+ addLayer(options = {}) {
1822
+ this.requireIdle("adding a layer");
1823
+ const count = _chunkWSTEXEKDcjs.flattenLayers.call(void 0, this.doc.layers).length;
1824
+ const type = _nullishCoalesce(options.type, () => ( "pixel"));
1825
+ const name = _optionalChain([options, 'access', _37 => _37.name, 'optionalAccess', _38 => _38.trim, 'call', _39 => _39()]) || (type === "group" ? `Group ${count + 1}` : `Layer ${count + 1}`);
1826
+ const layer = type === "group" ? { type: "group", id: newPaintLayerId(), name, visible: true, opacity: 1, blend: "pass-through", children: [] } : createPaintLayer(name);
1827
+ let parentId = null;
1828
+ let index;
1829
+ const above = _nullishCoalesce(options.aboveId, () => ( (options.parentId === void 0 ? this.doc.activeLayerId : null)));
1830
+ if (options.parentId !== void 0 && options.parentId !== null) {
1831
+ const g = this.requireLayer(options.parentId);
1832
+ if (g.type !== "group") throw new PaintEditorError(`Layer "${g.name}" is not a group; pass a group id as parentId.`);
1833
+ parentId = g.id;
1834
+ } else if (above) {
1835
+ const loc = _chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, above);
1836
+ if (!loc) throw new PaintEditorError(`Unknown layer "${above}". Call list_layers for the ids.`);
1837
+ parentId = _nullishCoalesce(_optionalChain([loc, 'access', _40 => _40.parent, 'optionalAccess', _41 => _41.id]), () => ( null));
1838
+ index = loc.index + 1;
1839
+ }
1840
+ const layers = _chunkWSTEXEKDcjs.insertLayer.call(void 0, this.doc.layers, layer, parentId, index);
1841
+ this.commit(type === "group" ? "Add group" : "Add layer", { ...this.doc, layers, activeLayerId: layer.id });
1842
+ return this.describeLayer(layer);
1843
+ }
1844
+ updateLayer(id, update) {
1845
+ this.requireIdle("changing a layer");
1846
+ const l = this.requireLayer(id);
1847
+ if (_chunkWSTEXEKDcjs.isLayerLocked.call(void 0, l, "all") && Object.keys(update).some((k) => k !== "locks")) throw new PaintEditorError(`Layer "${l.name}" is fully locked; unlock it first (locks {all: false}).`);
1848
+ if (update.opacity !== void 0 && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);
1849
+ if (update.blend !== void 0 && !_chunkCRMAXIUVcjs.isBlendMode.call(void 0, update.blend) && !(l.type === "group" && update.blend === "pass-through")) {
1850
+ throw new PaintEditorError(`Unknown blend mode "${update.blend}". Call get_canvas_info for the blend modes.`);
1851
+ }
1852
+ if (update.name !== void 0 && !update.name.trim()) throw new PaintEditorError("name is empty; give the layer a name.");
1853
+ const next = { ...l };
1854
+ if (update.name !== void 0) next.name = update.name.trim();
1855
+ if (update.visible !== void 0) next.visible = update.visible;
1856
+ if (update.opacity !== void 0) next.opacity = update.opacity;
1857
+ if (update.blend !== void 0) next.blend = update.blend;
1858
+ if (update.clipped !== void 0) next.clipped = update.clipped || void 0;
1859
+ if (update.locks !== void 0) {
1860
+ const locks = { ..._nullishCoalesce(l.locks, () => ( {})), ...update.locks };
1861
+ next.locks = Object.values(locks).some(Boolean) ? locks : void 0;
1862
+ }
1863
+ this.commit("Layer properties", { ...this.doc, layers: _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, id, () => next) });
1864
+ return this.describeLayer(next);
1865
+ }
1866
+ removeLayer(id) {
1867
+ this.requireIdle("removing a layer");
1868
+ const l = this.requireLayer(id);
1869
+ if (_chunkWSTEXEKDcjs.isLayerLocked.call(void 0, l, "all")) throw new PaintEditorError(`Layer "${l.name}" is fully locked; unlock it before removing it.`);
1870
+ const layers = _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, id, () => null);
1871
+ let active = this.doc.activeLayerId;
1872
+ if (active && !_chunkWSTEXEKDcjs.findLayer.call(void 0, layers, active)) active = _nullishCoalesce(_optionalChain([_chunkWSTEXEKDcjs.flattenLayers.call(void 0, layers), 'access', _42 => _42.filter, 'call', _43 => _43((x) => x.type === "pixel"), 'access', _44 => _44.pop, 'call', _45 => _45(), 'optionalAccess', _46 => _46.id]), () => ( null));
1873
+ this.commit("Remove layer", { ...this.doc, layers, activeLayerId: active });
1874
+ return { removed: id, activeLayerId: active };
1875
+ }
1876
+ duplicateLayer(id) {
1877
+ this.requireIdle("duplicating a layer");
1878
+ const loc = _chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, id);
1879
+ if (!loc) throw new PaintEditorError(`Unknown layer "${id}". Call list_layers for the ids.`);
1880
+ const copy = (l) => l.type === "group" ? { ...l, id: newPaintLayerId(), children: l.children.map(copy) } : { ...l, id: newPaintLayerId() };
1881
+ const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };
1882
+ const layers = _chunkWSTEXEKDcjs.insertLayer.call(void 0, this.doc.layers, dup, _nullishCoalesce(_optionalChain([loc, 'access', _47 => _47.parent, 'optionalAccess', _48 => _48.id]), () => ( null)), loc.index + 1);
1883
+ this.commit("Duplicate layer", { ...this.doc, layers, activeLayerId: dup.id });
1884
+ return this.describeLayer(dup);
1885
+ }
1886
+ /** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */
1887
+ moveLayer(id, index, parentId) {
1888
+ this.requireIdle("moving a layer");
1889
+ const loc = _chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, id);
1890
+ if (!loc) throw new PaintEditorError(`Unknown layer "${id}". Call list_layers for the ids.`);
1891
+ const target = parentId === void 0 ? _nullishCoalesce(_optionalChain([loc, 'access', _49 => _49.parent, 'optionalAccess', _50 => _50.id]), () => ( null)) : parentId;
1892
+ if (target) {
1893
+ const g = this.requireLayer(target);
1894
+ if (g.type !== "group") throw new PaintEditorError(`Layer "${g.name}" is not a group; pass a group id as parentId.`);
1895
+ if (target === id || loc.layer.type === "group" && _chunkWSTEXEKDcjs.findLayer.call(void 0, loc.layer.children, target)) throw new PaintEditorError("A group cannot move into itself.");
1896
+ }
1897
+ const without = _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, id, () => null);
1898
+ const siblings = target ? _chunkWSTEXEKDcjs.findLayer.call(void 0, without, target).layer.children : without;
1899
+ 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).`);
1900
+ const layers = _chunkWSTEXEKDcjs.insertLayer.call(void 0, without, loc.layer, target, index);
1901
+ this.commit("Move layer", { ...this.doc, layers });
1902
+ return this.describeLayer(loc.layer);
1903
+ }
1904
+ /** Merges a pixel layer into the pixel layer below it (keeping the lower one's settings). */
1905
+ mergeDown(id) {
1906
+ this.requireIdle("merging");
1907
+ const loc = _chunkWSTEXEKDcjs.findLayer.call(void 0, this.doc.layers, id);
1908
+ if (!loc) throw new PaintEditorError(`Unknown layer "${id}". Call list_layers for the ids.`);
1909
+ const siblings = loc.parent ? loc.parent.children : this.doc.layers;
1910
+ const below = siblings[loc.index - 1];
1911
+ const upper = loc.layer;
1912
+ if (!isPixelLayer(upper) || !isPixelLayer(below)) throw new PaintEditorError("Merge down needs a pixel layer directly above another pixel layer in the same group.");
1913
+ if (_chunkWSTEXEKDcjs.isLayerLocked.call(void 0, below, "pixels")) throw new PaintEditorError(`Layer "${below.name}" has its pixels locked; unlock it to merge into it.`);
1914
+ const rect = unionRect(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);
1915
+ let surface = _chunkCRMAXIUVcjs.emptySurface.call(void 0, 4);
1916
+ if (rect) {
1917
+ const base = { ...below, opacity: 1, blend: "normal", visible: true, clipped: void 0, mask: void 0 };
1918
+ const px = createPaintCompositor().composite([base, upper.visible ? upper : { ...upper, opacity: 0 }], rect, this.sources());
1919
+ surface = _chunkCRMAXIUVcjs.surfaceFromPixels.call(void 0, { width: rect.width, height: rect.height, data: px }, 4, rect.x, rect.y);
1920
+ }
1921
+ const merged = { ...below, surface, offset: { x: 0, y: 0 } };
1922
+ const layers = _chunkWSTEXEKDcjs.mapLayer.call(void 0, _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, upper.id, () => null), below.id, () => merged);
1923
+ this.commit("Merge down", { ...this.doc, layers, activeLayerId: below.id });
1924
+ return this.describeLayer(merged);
1925
+ }
1926
+ /** Removes every pixel of a layer. */
1927
+ clearLayer(layerId) {
1928
+ this.requireIdle("clearing");
1929
+ const l = this.paintTarget(layerId, "clear");
1930
+ const bounds = this.describeLayer(l).bounds;
1931
+ this.commit("Clear layer", { ...this.doc, layers: _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, l.id, () => ({ ...l, surface: _chunkCRMAXIUVcjs.emptySurface.call(void 0, 4) })) });
1932
+ return { layerId: l.id, bounds };
1933
+ }
1934
+ /**
1935
+ * Fills a layer across the canvas with a colour. With `grain` above 0 the fill follows the paper
1936
+ * grain (it catches the peaks, as pastel or charcoal rubbed across the sheet).
1937
+ */
1938
+ fillLayer(options = {}) {
1939
+ this.requireIdle("filling");
1940
+ const l = this.paintTarget(options.layerId, "fill");
1941
+ const color = options.color === void 0 ? this.color : parseColor(options.color);
1942
+ const opacity = _nullishCoalesce(options.opacity, () => ( 1));
1943
+ if (!(opacity >= 0 && opacity <= 1)) throw new PaintEditorError(`opacity ${opacity} is out of range; use 0..1.`);
1944
+ const grainAmount = _nullishCoalesce(options.grain, () => ( 0));
1945
+ if (!(grainAmount >= 0 && grainAmount <= 1)) throw new PaintEditorError(`grain ${grainAmount} is out of range; use 0..1.`);
1946
+ const pressure = _nullishCoalesce(options.pressure, () => ( 0.5));
1947
+ const blend = _nullishCoalesce(options.blend, () => ( "normal"));
1948
+ if (!_chunkCRMAXIUVcjs.isBlendMode.call(void 0, blend)) throw new PaintEditorError(`Unknown blend mode "${blend}".`);
1949
+ const { width, height } = this.doc.canvas;
1950
+ const rect = { x: 0, y: 0, width, height };
1951
+ const coverage = new (0, _chunkPV42JITTcjs.CoverageMap)();
1952
+ const depth = grainAmount * this.doc.paper.depth;
1953
+ const grain = depth > 0 ? paperGrain(this.doc.paper) : null;
1954
+ for (const { tx, ty } of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, rect)) {
1955
+ const r = { x: tx * _chunkCRMAXIUVcjs.TILE, y: ty * _chunkCRMAXIUVcjs.TILE, width: Math.min(_chunkCRMAXIUVcjs.TILE, width - tx * _chunkCRMAXIUVcjs.TILE), height: Math.min(_chunkCRMAXIUVcjs.TILE, height - ty * _chunkCRMAXIUVcjs.TILE) };
1956
+ const vals = new Float32Array(r.width * r.height);
1957
+ 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;
1958
+ coverage.accumulate(r, vals, 1);
1959
+ }
1960
+ const apply = {
1961
+ mode: "paint",
1962
+ color: rgbaOf(color),
1963
+ background: rgbaOf(this.doc.paper.color),
1964
+ opacity,
1965
+ blend,
1966
+ lockTransparency: _chunkWSTEXEKDcjs.isLayerLocked.call(void 0, l, "transparency"),
1967
+ selection: null,
1968
+ offset: l.offset
1969
+ };
1970
+ const surface = this.applyCoverage(l, coverage, rect, apply);
1971
+ this.commit("Fill layer", { ...this.doc, layers: _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, l.id, () => ({ ...l, surface })) });
1972
+ return { layerId: l.id, bounds: rect };
1973
+ }
1974
+ applyCoverage(l, coverage, rect, apply) {
1975
+ const base = l.surface;
1976
+ const tiles = new Map(base.tiles);
1977
+ for (const { tx, ty } of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, { x: rect.x - l.offset.x, y: rect.y - l.offset.y, width: rect.width, height: rect.height })) {
1978
+ const key = _chunkCRMAXIUVcjs.tileKey.call(void 0, tx, ty);
1979
+ const src = _nullishCoalesce(_optionalChain([base, 'access', _51 => _51.tiles, 'access', _52 => _52.get, 'call', _53 => _53(key), 'optionalAccess', _54 => _54.data]), () => ( new Uint8ClampedArray(_chunkCRMAXIUVcjs.TILE * _chunkCRMAXIUVcjs.TILE * 4)));
1980
+ const tile = _chunkCRMAXIUVcjs.finishTile.call(void 0, _chunkPV42JITTcjs.applyStrokeToTile.call(void 0, coverage, src, 4, tx, ty, apply), 4, 0);
1981
+ if (tile) tiles.set(key, tile);
1982
+ else tiles.delete(key);
1983
+ }
1984
+ return new (0, _chunkCRMAXIUVcjs.TiledSurface)(4, tiles, 0);
1985
+ }
1986
+ // --- Brushes -------------------------------------------------------------------
1987
+ findBrush(id) {
1988
+ return _nullishCoalesce(BUILTIN_BRUSHES.find((b) => b.id === id), () => ( this.userBrushes.find((b) => b.id === id)));
1989
+ }
1990
+ /** A library brush by id (`undefined` when unknown). */
1991
+ getBrush(id) {
1992
+ const b = this.findBrush(id);
1993
+ return b ? normalizePaintBrush(b, b) : void 0;
1994
+ }
1995
+ requireBrush(id) {
1996
+ const b = this.findBrush(id);
1997
+ if (!b) throw new PaintEditorError(`Unknown brush "${id}". Call list_brushes for the ids.`);
1998
+ return b;
1999
+ }
2000
+ /**
2001
+ * Chooses the active brush (`id`: a library brush) and / or changes its settings. Editor state,
2002
+ * not an undo step. Returns the active brush.
2003
+ */
2004
+ setBrush(update = {}) {
2005
+ const { id, ...changes } = update;
2006
+ const base = id !== void 0 ? this.requireBrush(id) : this.brush;
2007
+ const next = normalizePaintBrush({ ...changes, id: base.id }, base);
2008
+ if (base.builtin && !Object.keys(changes).length) next.builtin = true;
2009
+ else delete next.builtin;
2010
+ this.brush = next;
2011
+ this.emit([]);
2012
+ return this.brush;
2013
+ }
2014
+ /** Saves the active brush (or `brush`) into the user library under `id` / `name`. */
2015
+ saveBrush(options) {
2016
+ 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").`);
2017
+ if (BUILTIN_BRUSHES.some((b) => b.id === options.id)) throw new PaintEditorError(`"${options.id}" is a built-in brush; save under another id.`);
2018
+ const source = options.brush ? normalizePaintBrush(options.brush, this.brush) : this.brush;
2019
+ const saved = normalizePaintBrush({ ...source, id: options.id, name: _nullishCoalesce(options.name, () => ( source.name)) });
2020
+ delete saved.builtin;
2021
+ const i = this.userBrushes.findIndex((b) => b.id === options.id);
2022
+ if (i >= 0) this.userBrushes[i] = saved;
2023
+ else this.userBrushes.push(saved);
2024
+ this.emit([]);
2025
+ return saved;
2026
+ }
2027
+ removeBrush(id) {
2028
+ if (BUILTIN_BRUSHES.some((b) => b.id === id)) throw new PaintEditorError(`"${id}" is a built-in brush and cannot be removed.`);
2029
+ const i = this.userBrushes.findIndex((b) => b.id === id);
2030
+ if (i < 0) throw new PaintEditorError(`Unknown brush "${id}". Call list_brushes for the ids.`);
2031
+ this.userBrushes.splice(i, 1);
2032
+ this.emit([]);
2033
+ }
2034
+ /** The user's saved brushes. */
2035
+ getUserBrushes() {
2036
+ return this.userBrushes.map((b) => normalizePaintBrush(b, b));
2037
+ }
2038
+ /** A brush library file of the given brushes (default: the user's brushes). */
2039
+ exportBrushes(ids) {
2040
+ const list = ids ? ids.map((id) => this.requireBrush(id)) : this.userBrushes;
2041
+ return { format: "pixerate-brushes", version: 1, brushes: list.map(brushToJSON) };
2042
+ }
2043
+ /**
2044
+ * Adds the brushes of a library file to the user library. A brush whose id is taken is skipped
2045
+ * unless `replace` (built-in ids are always skipped). Returns the ids added and the problems.
2046
+ */
2047
+ importBrushes(file, options = {}) {
2048
+ const problems = [];
2049
+ let list;
2050
+ try {
2051
+ list = parseBrushLibrary(file, problems);
2052
+ } catch (e) {
2053
+ throw new PaintEditorError(e.message);
2054
+ }
2055
+ const added = [];
2056
+ const skipped = [];
2057
+ for (const b of list) {
2058
+ if (BUILTIN_BRUSHES.some((x) => x.id === b.id)) {
2059
+ skipped.push(b.id);
2060
+ problems.push(`"${b.id}" is a built-in brush id; rename it to import it.`);
2061
+ continue;
2062
+ }
2063
+ const i = this.userBrushes.findIndex((x) => x.id === b.id);
2064
+ if (i >= 0 && !options.replace) {
2065
+ skipped.push(b.id);
2066
+ continue;
2067
+ }
2068
+ if (i >= 0) this.userBrushes[i] = b;
2069
+ else this.userBrushes.push(b);
2070
+ added.push(b.id);
2071
+ }
2072
+ if (added.length) this.emit([]);
2073
+ return { added, skipped, problems };
2074
+ }
2075
+ /** A thumbnail of a brush: a test stroke on the current paper. */
2076
+ brushThumbnail(id, options = {}) {
2077
+ const b = id === this.brush.id ? this.brush : this.requireBrush(id);
2078
+ return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
2079
+ }
2080
+ // --- Colour --------------------------------------------------------------------
2081
+ setColor(color) {
2082
+ this.color = parseColor(color);
2083
+ this.emit([]);
2084
+ return this.color;
2085
+ }
2086
+ setSecondaryColor(color) {
2087
+ this.secondaryColor = parseColor(color);
2088
+ this.emit([]);
2089
+ return this.secondaryColor;
2090
+ }
2091
+ swapColors() {
2092
+ [this.color, this.secondaryColor] = [this.secondaryColor, this.color];
2093
+ this.emit([]);
2094
+ }
2095
+ /** The colour at a document pixel: of the composite (paper included) or of one layer. */
2096
+ pickColor(x, y, options = {}) {
2097
+ const { width, height } = this.doc.canvas;
2098
+ const px = Math.floor(x);
2099
+ const py = Math.floor(y);
2100
+ if (!(px >= 0 && py >= 0 && px < width && py < height)) throw new PaintEditorError(`(${x}, ${y}) is outside the ${width}\xD7${height} canvas.`);
2101
+ if (options.layerId !== void 0) {
2102
+ const l = this.requireLayer(options.layerId);
2103
+ if (!isPixelLayer(l)) throw new PaintEditorError(`Layer "${l.name}" is a group; pick from a pixel layer or the composite.`);
2104
+ const [r, g, b, a] = _chunkCRMAXIUVcjs.readPixel.call(void 0, l.surface, px - l.offset.x, py - l.offset.y);
2105
+ return { color: rgbToHex(r, g, b), alpha: Math.round(a / 255 * 1e3) / 1e3 };
2106
+ }
2107
+ const d = this.exportCompositor.composite(this.renderLayers({}), { x: px, y: py, width: 1, height: 1 }, this.sources());
2108
+ return { color: rgbToHex(d[0], d[1], d[2]), alpha: Math.round(d[3] / 255 * 1e3) / 1e3 };
2109
+ }
2110
+ // --- Mixer Brush ---------------------------------------------------------------
2111
+ /** Fills the Mixer Brush reservoir with a colour (default the editor colour). */
2112
+ loadMixerBrush(color) {
2113
+ const [r, g, b] = hexToRgb8(color === void 0 ? this.color : parseColor(color));
2114
+ this.mixerLoad = _chunkPV42JITTcjs.loadMixer.call(void 0, this.mixerLoad, [r / 255, g / 255, b / 255]);
2115
+ this.emit([]);
2116
+ return this.mixerLoad;
2117
+ }
2118
+ /** Empties the pickup well (and with `emptyReservoir` the loaded paint too). */
2119
+ cleanMixerBrush(emptyReservoir = false) {
2120
+ this.mixerLoad = emptyReservoir ? _chunkPV42JITTcjs.EMPTY_MIXER_LOAD : _chunkPV42JITTcjs.cleanMixer.call(void 0, this.mixerLoad);
2121
+ this.emit([]);
2122
+ return this.mixerLoad;
2123
+ }
2124
+ // --- View and hand -------------------------------------------------------------
2125
+ /** Zoom, pan, rotation and mirroring: view state only, the pixels never resample. */
2126
+ setView(update) {
2127
+ this.view = normalizeView(update, this.view);
2128
+ this.emit([]);
2129
+ return this.view;
2130
+ }
2131
+ resetView() {
2132
+ return this.setView({ ...DEFAULT_VIEW, pan: { x: 0, y: 0 } });
2133
+ }
2134
+ setHand(hand) {
2135
+ if (hand !== "left" && hand !== "right") throw new PaintEditorError(`hand is ${JSON.stringify(hand)}; use "left" or "right".`);
2136
+ this.hand = hand;
2137
+ this.emit([]);
2138
+ return hand;
2139
+ }
2140
+ // --- Strokes -------------------------------------------------------------------
2141
+ strokeBrush(init) {
2142
+ let brush = init.brushId !== void 0 ? this.requireBrush(init.brushId) : this.brush;
2143
+ if (init.brush) brush = normalizePaintBrush({ ...init.brush, id: brush.id }, brush);
2144
+ return brush;
2145
+ }
2146
+ /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
2147
+ strokeCopies() {
2148
+ return [];
2149
+ }
2150
+ sessionFor(doc, init, host) {
2151
+ const layer = (() => {
2152
+ const id = _nullishCoalesce(init.layerId, () => ( doc.activeLayerId));
2153
+ if (!id) throw new PaintEditorError("There is no active layer to paint on; add one with add_layer.");
2154
+ const loc = _chunkWSTEXEKDcjs.findLayer.call(void 0, doc.layers, id);
2155
+ if (!loc) throw new PaintEditorError(`Unknown layer "${id}". Call list_layers for the ids.`);
2156
+ const l = loc.layer;
2157
+ if (!isPixelLayer(l)) throw new PaintEditorError(`Layer "${l.name}" is a group; paint on a pixel layer inside it.`);
2158
+ if (_chunkWSTEXEKDcjs.isLayerLocked.call(void 0, l, "pixels")) throw new PaintEditorError(`Layer "${l.name}" has its pixels locked; unlock it (update_layer locks) to paint on it.`);
2159
+ if (!l.visible) throw new PaintEditorError(`Layer "${l.name}" is hidden; show it (update_layer visible true) to paint on it.`);
2160
+ return l;
2161
+ })();
2162
+ const brush = this.strokeBrush(init);
2163
+ const color = init.color === void 0 ? this.color : parseColor(init.color);
2164
+ const rgba = rgbaOf(color);
2165
+ const seed = _nullishCoalesce(init.seed, () => ( mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647));
2166
+ const copies = (_nullishCoalesce(init.copies, () => ( this.strokeCopies()))).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
2167
+ const session = new PaintStrokeSession(host, {
2168
+ layerId: layer.id,
2169
+ surface: layer.surface,
2170
+ offset: layer.offset,
2171
+ lockTransparency: _chunkWSTEXEKDcjs.isLayerLocked.call(void 0, layer, "transparency"),
2172
+ brush,
2173
+ color: rgba,
2174
+ background: rgbaOf(doc.paper.color),
2175
+ grain: paperGrain(doc.paper),
2176
+ paperDepth: doc.paper.depth,
2177
+ seed,
2178
+ colorHex: color,
2179
+ affineCopies: copies,
2180
+ copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
2181
+ mixerLoad: brush.engine === "mixer" ? mixerLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]) : _chunkPV42JITTcjs.EMPTY_MIXER_LOAD
2182
+ });
2183
+ return session;
2184
+ }
2185
+ record(session) {
2186
+ const init = session.init;
2187
+ const stroke = {
2188
+ id: `s${this.strokeCounter++}`,
2189
+ layerId: session.layerId,
2190
+ brush: brushToJSON(init.brush),
2191
+ color: init.colorHex,
2192
+ points: session.points.map((p) => ({ ...p })),
2193
+ seed: init.seed
2194
+ };
2195
+ if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m]);
2196
+ return stroke;
2197
+ }
2198
+ /**
2199
+ * Starts a live stroke for pointer input: `push` raw pen samples (document pixels), then `end()`
2200
+ * (one undo step) or `cancel()`.
2201
+ */
2202
+ beginStroke(init = {}) {
2203
+ this.requireIdle("starting another");
2204
+ const host = {
2205
+ update: (layerId, surface, rects) => {
2206
+ this.live = { layerId, surface };
2207
+ this.emit(rects);
2208
+ },
2209
+ commit: (s, surface, cancelled) => {
2210
+ this.session = null;
2211
+ this.live = null;
2212
+ this.liveLayers = null;
2213
+ const brush = s.init.brush;
2214
+ if (!cancelled && _optionalChain([s, 'access', _55 => _55.result, 'optionalAccess', _56 => _56.mixerLoad]) && brush.loadsColor) this.mixerLoad = s.result.mixerLoad;
2215
+ if (cancelled || !surface) {
2216
+ this.emit();
2217
+ return;
2218
+ }
2219
+ const stroke = this.record(s);
2220
+ s.result.strokeId = stroke.id;
2221
+ const l = this.getLayer(s.layerId);
2222
+ this.commit(`${brush.name} stroke`, { ...this.doc, layers: _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke] });
2223
+ }
2224
+ };
2225
+ const session = this.sessionFor(this.doc, init, host);
2226
+ this.session = session;
2227
+ this.emit([]);
2228
+ return session;
2229
+ }
2230
+ /** The current live stroke, if any. */
2231
+ get activeStroke() {
2232
+ return this.session;
2233
+ }
2234
+ /** Runs a whole stroke against `doc` without touching the editor; returns the new document. */
2235
+ runStroke(doc, init, points) {
2236
+ let out = doc;
2237
+ const host = {
2238
+ update: () => {
2239
+ },
2240
+ commit: (s, surface, cancelled) => {
2241
+ if (cancelled || !surface) return;
2242
+ const stroke = this.record(s);
2243
+ s.result.strokeId = stroke.id;
2244
+ const l = _chunkWSTEXEKDcjs.findLayer.call(void 0, out.layers, s.layerId).layer;
2245
+ out = { ...out, layers: _chunkWSTEXEKDcjs.mapLayer.call(void 0, out.layers, l.id, () => ({ ...l, surface })), strokes: [...out.strokes, stroke] };
2246
+ }
2247
+ };
2248
+ const session = this.sessionFor(doc, init, host);
2249
+ session.push(points);
2250
+ const result = session.end();
2251
+ return { doc: out, result, brush: session.init.brush };
2252
+ }
2253
+ /** Paints one stroke from data (one undo step). */
2254
+ paintStroke(init) {
2255
+ this.requireIdle("painting");
2256
+ const points = this.checkPoints(init.points);
2257
+ const { doc, result, brush } = this.runStroke(this.doc, init, points);
2258
+ if (result.mixerLoad && brush.loadsColor) this.mixerLoad = result.mixerLoad;
2259
+ if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);
2260
+ else this.emit([]);
2261
+ return result;
2262
+ }
2263
+ checkPoints(points) {
2264
+ if (!Array.isArray(points) || !points.length) throw new PaintEditorError("points is empty; give at least one {x, y} in document pixels.");
2265
+ const out = [];
2266
+ points.forEach((p, i) => {
2267
+ if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) throw new PaintEditorError(`points[${i}] needs finite x and y in document pixels.`);
2268
+ if (p.pressure !== void 0 && !(p.pressure >= 0 && p.pressure <= 1)) throw new PaintEditorError(`points[${i}].pressure ${p.pressure} is out of range; use 0..1.`);
2269
+ out.push(p);
2270
+ });
2271
+ return out;
2272
+ }
2273
+ /** Every recorded stroke, oldest first. */
2274
+ getStrokes() {
2275
+ return this.doc.strokes;
2276
+ }
2277
+ /**
2278
+ * Replays recorded strokes, varied (moved, scaled, rotated, flipped, recoloured, with another
2279
+ * brush or layer), `repeat` times: all of it one undo step. Returns the new strokes.
2280
+ */
2281
+ replayStrokes(options) {
2282
+ this.requireIdle("replaying");
2283
+ if (!_optionalChain([options, 'access', _57 => _57.strokeIds, 'optionalAccess', _58 => _58.length])) throw new PaintEditorError("strokeIds is empty; pass ids from list_strokes.");
2284
+ const strokes = options.strokeIds.map((id) => {
2285
+ const s = this.doc.strokes.find((x) => x.id === id);
2286
+ if (!s) throw new PaintEditorError(`Unknown stroke "${id}". Call list_strokes for the ids.`);
2287
+ return s;
2288
+ });
2289
+ const repeat = _nullishCoalesce(options.repeat, () => ( 1));
2290
+ if (!Number.isInteger(repeat) || repeat < 1 || repeat > 100) throw new PaintEditorError(`repeat ${repeat} is out of range; use 1..100.`);
2291
+ if (options.scale !== void 0 && !(options.scale > 0 && options.scale <= 20)) throw new PaintEditorError(`scale ${options.scale} is out of range; use above 0 up to 20.`);
2292
+ if (options.color !== void 0) parseColor(options.color);
2293
+ if (options.brushId !== void 0) this.requireBrush(options.brushId);
2294
+ if (options.layerId !== void 0) this.paintTarget(options.layerId, "replay onto");
2295
+ let origin = options.origin;
2296
+ if (!origin) {
2297
+ const b = strokeBounds(strokes.flatMap((s) => s.points));
2298
+ origin = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
2299
+ }
2300
+ const step = variationAffine(options, origin);
2301
+ let doc = this.doc;
2302
+ let bounds = null;
2303
+ const ids = [];
2304
+ const layerIds = /* @__PURE__ */ new Set();
2305
+ let m = IDENTITY_AFFINE;
2306
+ for (let k = 0; k < repeat; k++) {
2307
+ 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]];
2308
+ for (const s of strokes) {
2309
+ const v = varyStroke(s, m, options);
2310
+ let brush = v.brush;
2311
+ if (options.brushId !== void 0) {
2312
+ const b = this.requireBrush(options.brushId);
2313
+ brush = normalizePaintBrush({ tip: { ...b.tip, size: b.tip.size * (v.brush.tip.size / s.brush.tip.size) } }, b);
2314
+ }
2315
+ const layerId = _nullishCoalesce(options.layerId, () => ( s.layerId));
2316
+ if (!_chunkWSTEXEKDcjs.findLayer.call(void 0, doc.layers, layerId)) throw new PaintEditorError(`Stroke "${s.id}" was painted on layer "${layerId}", which no longer exists; pass layerId to replay it elsewhere.`);
2317
+ const run = this.runStroke(doc, { layerId, color: _nullishCoalesce(options.color, () => ( s.color)), brush, seed: s.seed, copies: _nullishCoalesce(v.copies, () => ( [])) }, v.points);
2318
+ doc = run.doc;
2319
+ bounds = unionRect(bounds, run.result.bounds);
2320
+ if (run.result.strokeId) ids.push(run.result.strokeId);
2321
+ layerIds.add(layerId);
2322
+ }
2323
+ }
2324
+ if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? "Replay stroke" : "Replay strokes", doc);
2325
+ return { strokeIds: ids, bounds, layerIds: [...layerIds] };
2326
+ }
2327
+ // --- Rendering -----------------------------------------------------------------
2328
+ sources() {
2329
+ return { width: this.doc.canvas.width, height: this.doc.canvas.height };
2330
+ }
2331
+ /** The layers with the live stroke's surface in place. */
2332
+ currentLayers() {
2333
+ const live = this.live;
2334
+ if (!live) return this.doc.layers;
2335
+ if (_optionalChain([this, 'access', _59 => _59.liveLayers, 'optionalAccess', _60 => _60.key]) === live.surface) return this.liveLayers.layers;
2336
+ const layers = _chunkWSTEXEKDcjs.mapLayer.call(void 0, this.doc.layers, live.layerId, (l) => ({ ...l, surface: live.surface }));
2337
+ this.liveLayers = { key: live.surface, layers };
2338
+ return layers;
2339
+ }
2340
+ /** View-only layers between the paper and the paint (the painting guides add the tracing image). */
2341
+ underlayLayers() {
2342
+ return [];
2343
+ }
2344
+ renderLayers(options, view = false) {
2345
+ let layers = this.currentLayers();
2346
+ if (options.layerIds) {
2347
+ const picked = [];
2348
+ for (const id of options.layerIds) picked.push(this.requireLayer(id));
2349
+ layers = picked.map((l) => ({ ...l, visible: true }));
2350
+ }
2351
+ const under = options.paper === false ? [] : [paperNode(this.doc.paper)];
2352
+ if (view) under.push(...this.underlayLayers());
2353
+ return [...under, ...layers];
2354
+ }
2355
+ /**
2356
+ * Composites the viewport stack (paper, view-only underlays, layers with the live stroke) tile by
2357
+ * tile over `rect`, calling `onTile` with each 256 px tile's RGBA (cached: unchanged tiles come back
2358
+ * as the same buffer). Pass `takeDirty()` as `dirty`.
2359
+ */
2360
+ compositeTiles(rect, onTile, options = {}) {
2361
+ const dirty = options.dirty === "all" ? void 0 : options.dirty;
2362
+ this.viewCompositor.compositeTiles(this.renderLayers({}, true), rect, this.sources(), onTile, { dirty, checkpointLayerId: _nullishCoalesce(_optionalChain([this, 'access', _61 => _61.live, 'optionalAccess', _62 => _62.layerId]), () => ( this.doc.activeLayerId)) });
2363
+ }
2364
+ /** The canvas as canvas-sized RGBA (paper included by default). */
2365
+ compositePixels(options = {}) {
2366
+ const { width, height } = this.doc.canvas;
2367
+ const data = this.exportCompositor.composite(this.renderLayers(options), { x: 0, y: 0, width, height }, this.sources());
2368
+ return { width, height, data };
2369
+ }
2370
+ /** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */
2371
+ renderPreview(options = {}) {
2372
+ const full = this.compositePixels(options);
2373
+ const side = Math.max(1, Math.round(_nullishCoalesce(options.maxSide, () => ( 512))));
2374
+ const k = Math.min(1, side / Math.max(full.width, full.height));
2375
+ if (k >= 1) return full;
2376
+ const w = Math.max(1, Math.round(full.width * k));
2377
+ const h = Math.max(1, Math.round(full.height * k));
2378
+ return { width: w, height: h, data: downsampleRgba(full.data, full.width, full.height, w, h) };
2379
+ }
2380
+ // --- Save, load and export -------------------------------------------------------
2381
+ /** The document as JSON with inline PNG assets. */
2382
+ toJSON(options = {}) {
2383
+ return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions });
2384
+ }
2385
+ /** The document as JSON with each PNG stored in `store`. */
2386
+ toJSONExternal(store, options = {}) {
2387
+ return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions });
2388
+ }
2389
+ adopt(loaded) {
2390
+ this.extensions = loaded.extensions;
2391
+ for (const s of loaded.state.strokes) {
2392
+ const n = Number(_nullishCoalesce(_optionalChain([/^s(\d+)$/, 'access', _63 => _63.exec, 'call', _64 => _64(s.id), 'optionalAccess', _65 => _65[1]]), () => ( 0)));
2393
+ if (n >= this.strokeCounter) this.strokeCounter = n + 1;
2394
+ }
2395
+ this.undoStack = [];
2396
+ this.redoStack = [];
2397
+ this.doc = loaded.state;
2398
+ this.viewCompositor.clear();
2399
+ this.emit();
2400
+ return loaded.problems;
2401
+ }
2402
+ /** Opens a saved document (clears the history). Returns what could not be read. */
2403
+ loadJSON(raw) {
2404
+ this.requireIdle("opening a document");
2405
+ try {
2406
+ return this.adopt(deserializePaintDocument(raw));
2407
+ } catch (e) {
2408
+ throw new PaintEditorError(e.message);
2409
+ }
2410
+ }
2411
+ /** Opens a saved document whose assets may live in `store`. */
2412
+ async loadJSONAsync(raw, store) {
2413
+ this.requireIdle("opening a document");
2414
+ try {
2415
+ return this.adopt(await deserializePaintDocumentAsync(raw, store));
2416
+ } catch (e) {
2417
+ throw new PaintEditorError(e.message);
2418
+ }
2419
+ }
2420
+ /** The canvas as PNG bytes (paper included by default; `scale` below 1 shrinks it). */
2421
+ exportPng(options = {}) {
2422
+ const img = this.scaled(options);
2423
+ return _chunkWSTEXEKDcjs.encodePng.call(void 0, img.width, img.height, img.data);
2424
+ }
2425
+ scaled(options) {
2426
+ const full = this.compositePixels(options);
2427
+ const s = _nullishCoalesce(options.scale, () => ( 1));
2428
+ if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);
2429
+ if (s === 1) return full;
2430
+ const w = Math.max(1, Math.round(full.width * s));
2431
+ const h = Math.max(1, Math.round(full.height * s));
2432
+ return { width: w, height: h, data: downsampleRgba(full.data, full.width, full.height, w, h) };
2433
+ }
2434
+ /**
2435
+ * The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.
2436
+ * JPEG and WebP need a canvas (a browser or Worker) or the `encodeImage` option.
2437
+ */
2438
+ async exportImage(options = {}) {
2439
+ const format = _nullishCoalesce(options.format, () => ( "image/png"));
2440
+ if (format === "image/png") return this.exportPng(options);
2441
+ 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.`);
2442
+ const quality = _nullishCoalesce(options.quality, () => ( 0.92));
2443
+ if (!(quality >= 0 && quality <= 1)) throw new PaintEditorError(`quality ${quality} is out of range; use 0..1.`);
2444
+ const img = this.scaled({ ...options, paper: format === "image/jpeg" ? true : options.paper });
2445
+ const encode = _nullishCoalesce(this.encodeImage, () => ( defaultImageEncoder()));
2446
+ if (!encode) throw new PaintEditorError(`${format} needs a canvas to encode and there is none here; pass encodeImage to the controller, or export PNG.`);
2447
+ return encode(img, format, quality);
2448
+ }
2449
+ /**
2450
+ * The document as an 8-bit RGB PSD through the shared PSD writer: every layer and group, with
2451
+ * the paper as a locked "Paper" pixel layer at the bottom (it is generated, so PSD gets its
2452
+ * pixels), and the composite as the merged image.
2453
+ */
2454
+ exportPsd() {
2455
+ const { width, height, dpi } = this.doc.canvas;
2456
+ const paperPixels = this.compositePixels({ layerIds: [], paper: true });
2457
+ const paperLayer = {
2458
+ type: "pixel",
2459
+ id: "paper",
2460
+ name: "Paper",
2461
+ visible: true,
2462
+ opacity: 1,
2463
+ blend: "normal",
2464
+ locks: { all: true },
2465
+ surface: _chunkCRMAXIUVcjs.surfaceFromPixels.call(void 0, paperPixels, 4),
2466
+ offset: { x: 0, y: 0 }
2467
+ };
2468
+ const merged = this.compositePixels({ paper: true }).data;
2469
+ const result = _chunkVSMM7B3Pcjs.exportPsd.call(void 0, { width, height, layers: [paperLayer, ...this.doc.layers], merged, resolution: dpi });
2470
+ const pigment = _chunkWSTEXEKDcjs.flattenLayers.call(void 0, this.doc.layers).filter((l) => l.blend === "pigment");
2471
+ 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.`);
2472
+ return result;
2473
+ }
2474
+ /** `render_preview` as a base64 PNG. */
2475
+ previewPngBase64(options = {}) {
2476
+ const img = this.renderPreview(options);
2477
+ return { data: _chunkWSTEXEKDcjs.bytesToBase64.call(void 0, _chunkWSTEXEKDcjs.encodePng.call(void 0, img.width, img.height, img.data)), width: img.width, height: img.height };
2478
+ }
2479
+ /** A layer's tile surface (live while stroking), for thumbnails. */
2480
+ layerSurface(id) {
2481
+ const l = _optionalChain([_chunkWSTEXEKDcjs.findLayer.call(void 0, this.currentLayers(), id), 'optionalAccess', _66 => _66.layer]);
2482
+ return isPixelLayer(l) ? l.surface : null;
2483
+ }
2484
+ }, _class7);
2485
+
2486
+ // src/paint-editor/tools.ts
2487
+ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
2488
+ var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
2489
+ var SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
2490
+ var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
2491
+ var EMPTY = { type: "object", properties: {}, additionalProperties: false };
2492
+ var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
2493
+ var LAYER_ID = { type: "string", description: "Layer id from list_layers (default the active layer)." };
2494
+ var TIP = {
2495
+ type: "object",
2496
+ description: "The tip (the shared brush engine). Missing fields keep their current values.",
2497
+ properties: {
2498
+ size: { type: "number", minimum: 1, maximum: _chunkPV42JITTcjs.MAX_BRUSH_SIZE, description: "Diameter in document pixels." },
2499
+ hardness: { type: "number", minimum: 0, maximum: 1, description: "Edge hardness, 0 (soft) .. 1 (hard)." },
2500
+ spacing: { type: "number", minimum: 0.01, maximum: 10, description: "Dab spacing as a fraction of the diameter (0.05 is smooth, 1 is dotted)." },
2501
+ 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." },
2502
+ roundness: { type: "number", minimum: 0.01, maximum: 1, description: "Tip roundness: 1 round, lower is a flat ellipse (a pastel or chisel tip)." },
2503
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Stroke opacity ceiling: overlapping dabs never exceed it." },
2504
+ flow: { type: "number", minimum: 0, maximum: 1, description: "Build-up per dab towards the opacity (low flow builds up softly, like an airbrush)." },
2505
+ smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Position smoothing of the input, 0..0.95." },
2506
+ pressureSize: { type: "boolean", description: "Pressure scales the size (down to dynamics.minSize)." },
2507
+ pressureOpacity: { type: "boolean", description: "Pressure scales the opacity." },
2508
+ blend: { type: "string", enum: _chunkCRMAXIUVcjs.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." }
2509
+ },
2510
+ additionalProperties: false
2511
+ };
2512
+ var DYNAMICS = {
2513
+ type: "object",
2514
+ description: "Pen dynamics and jitter. Missing fields keep their current values.",
2515
+ properties: {
2516
+ minSize: { type: "number", minimum: 0, maximum: 1, description: "Size at zero pressure as a fraction of the full size (when pressureSize)." },
2517
+ pressureFlow: { type: "number", minimum: 0, maximum: 1, description: "How much pressure scales flow." },
2518
+ tiltSize: { type: "number", minimum: 0, maximum: 1, description: "Laying the pen down widens the tip (up to 2.5\xD7)." },
2519
+ tiltRoundness: { type: "number", minimum: 0, maximum: 1, description: "Laying the pen down flattens the tip along the tilt (side-of-the-pencil shading)." },
2520
+ tiltOpacity: { type: "number", minimum: 0, maximum: 1, description: "Laying the pen down lightens the mark." },
2521
+ velocitySize: { type: "number", minimum: -1, maximum: 1, description: "Positive grows the tip on fast strokes, negative thins it (needs point times t)." },
2522
+ velocityOpacity: { type: "number", minimum: -1, maximum: 1, description: "Positive darkens fast marks, negative lightens them (needs point times t)." },
2523
+ sizeJitter: { type: "number", minimum: 0, maximum: 1, description: "Random size change per dab." },
2524
+ angleJitter: { type: "number", minimum: 0, maximum: 1, description: "Random angle per dab, as a fraction of \xB1180\xB0." },
2525
+ opacityJitter: { type: "number", minimum: 0, maximum: 1, description: "Random opacity drop per dab." },
2526
+ scatter: { type: "number", minimum: 0, maximum: 4, description: "Random offset across the stroke, in diameters." }
2527
+ },
2528
+ additionalProperties: false
2529
+ };
2530
+ var MIXER = {
2531
+ type: "object",
2532
+ description: "Mixer engine settings (Mixer Brush, blender, smudge). Missing fields keep their current values.",
2533
+ properties: {
2534
+ wet: { type: "number", minimum: 0, maximum: 1, description: "How much canvas colour each dab picks up, 0 (dry) .. 1." },
2535
+ 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." },
2536
+ mix: { type: "number", minimum: 0, maximum: 1, description: "How far each dab's paint moves from the reservoir towards the picked-up colour." },
2537
+ loadEachStroke: { type: "boolean", description: "Refill the reservoir with the colour before each stroke." },
2538
+ cleanEachStroke: { type: "boolean", description: "Empty the pickup well after each stroke." },
2539
+ mixing: { type: "string", enum: [..._chunkPV42JITTcjs.MIXING_MODELS], description: "rgb: RGB averages. pigment: mixes like paint (Kubelka-Munk)." }
2540
+ },
2541
+ additionalProperties: false
2542
+ };
2543
+ var BRUSH_FIELDS = {
2544
+ name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." },
2545
+ category: { type: "string", enum: [...PAINT_BRUSH_CATEGORIES], description: "Library category." },
2546
+ engine: { type: "string", enum: [...PAINT_BRUSH_ENGINES], description: "dab: paints dabs; eraser: removes paint; mixer: picks up and moves paint (Mixer Brush, blender, smudge)." },
2547
+ tip: TIP,
2548
+ 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." },
2549
+ pressureSmoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Pressure smoothing, separate from the tip position smoothing." },
2550
+ 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)." },
2551
+ dynamics: DYNAMICS,
2552
+ mixer: MIXER,
2553
+ loadsColor: { type: "boolean", description: "Mixer engine: load the colour each stroke (false for blender and smudge, which only move paint)." }
2554
+ };
2555
+ var POINTS = {
2556
+ type: "array",
2557
+ minItems: 1,
2558
+ maxItems: 2e4,
2559
+ description: "The path in document pixels (raw samples; the brush smooths them). One point paints one dab.",
2560
+ items: {
2561
+ type: "object",
2562
+ properties: {
2563
+ x: { type: "number", description: "Document x in pixels." },
2564
+ y: { type: "number", description: "Document y in pixels." },
2565
+ pressure: { type: "number", minimum: 0, maximum: 1, description: "Pen pressure 0..1 (default 1)." },
2566
+ t: { type: "number", description: "Time in milliseconds (optional; drives velocity dynamics, e.g. an ink pen thinning on fast strokes)." },
2567
+ tiltX: { type: "number", minimum: -90, maximum: 90, description: "Pen tilt in degrees towards +x (Pointer Events tiltX)." },
2568
+ tiltY: { type: "number", minimum: -90, maximum: 90, description: "Pen tilt in degrees towards +y (Pointer Events tiltY)." },
2569
+ altitude: { type: "number", minimum: 0, maximum: 1.5708, description: "Pen altitude in radians: 1.5708 upright, 0 lying flat (overrides tiltX / tiltY)." },
2570
+ azimuth: { type: "number", minimum: 0, maximum: 6.2832, description: "Where the pen leans, radians clockwise from +x (with altitude)." },
2571
+ twist: { type: "number", minimum: 0, maximum: 359, description: "Barrel rotation in degrees (brushes with rotation barrel)." }
2572
+ },
2573
+ required: ["x", "y"],
2574
+ additionalProperties: false
2575
+ }
2576
+ };
2577
+ function loadResult(load) {
2578
+ const hex = (c) => c ? "#" + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("") : null;
2579
+ return { reservoir: hex(load.reservoir), amount: Math.round(load.amount * 1e3) / 1e3, pickup: hex(load.pickup) };
2580
+ }
2581
+ function brushSummary(b) {
2582
+ 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) };
2583
+ }
2584
+ function createPaintEditorToolProvider(controller, options = {}) {
2585
+ const c = controller;
2586
+ const tools = [];
2587
+ const add = (t) => tools.push(t);
2588
+ add({
2589
+ name: "get_canvas_info",
2590
+ title: "Get the canvas",
2591
+ description: "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.",
2592
+ inputSchema: EMPTY,
2593
+ annotations: { title: "Get the canvas", ...READ },
2594
+ handler: () => {
2595
+ const s = c.getState();
2596
+ const d = s.document;
2597
+ return {
2598
+ width: d.canvas.width,
2599
+ height: d.canvas.height,
2600
+ dpi: d.canvas.dpi,
2601
+ paper: { preset: d.paper.preset, name: d.paper.name, color: d.paper.color, depth: d.paper.depth },
2602
+ activeLayerId: d.activeLayerId,
2603
+ layerCount: c.listLayers().length,
2604
+ brushId: s.brush.id,
2605
+ color: s.color,
2606
+ secondaryColor: s.secondaryColor,
2607
+ view: { zoom: s.view.zoom, rotation: s.view.rotation, mirror: s.view.mirror },
2608
+ hand: s.hand,
2609
+ strokes: d.strokes.length,
2610
+ undo: s.undoLabel,
2611
+ redo: s.redoLabel,
2612
+ blendModes: _chunkCRMAXIUVcjs.BLEND_MODES.map((m) => m.id)
2613
+ };
2614
+ }
2615
+ });
2616
+ add({
2617
+ name: "new_canvas",
2618
+ title: "Start a new canvas",
2619
+ description: "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.",
2620
+ inputSchema: {
2621
+ type: "object",
2622
+ properties: {
2623
+ width: { type: "integer", minimum: 1, maximum: MAX_CANVAS_SIDE, description: "Width in pixels (default the current width)." },
2624
+ height: { type: "integer", minimum: 1, maximum: MAX_CANVAS_SIDE, description: "Height in pixels (default the current height)." },
2625
+ dpi: { type: "number", minimum: 1, maximum: 1e4, description: "Pixels per inch for print sizes and PSD (default the current dpi)." },
2626
+ paper: { type: "string", description: "Paper preset id from list_papers (default the current paper)." }
2627
+ },
2628
+ additionalProperties: false
2629
+ },
2630
+ annotations: { title: "Start a new canvas", ...DESTRUCTIVE },
2631
+ handler: (a) => {
2632
+ const d = c.newCanvas({ width: a.width, height: a.height, dpi: a.dpi, paper: a.paper });
2633
+ return { width: d.canvas.width, height: d.canvas.height, dpi: d.canvas.dpi, paper: d.paper.preset, layerId: d.activeLayerId };
2634
+ }
2635
+ });
2636
+ add({
2637
+ name: "set_canvas",
2638
+ title: "Resize the canvas",
2639
+ description: "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.",
2640
+ inputSchema: {
2641
+ type: "object",
2642
+ properties: {
2643
+ width: { type: "integer", minimum: 1, maximum: MAX_CANVAS_SIDE, description: "Width in pixels (default unchanged)." },
2644
+ height: { type: "integer", minimum: 1, maximum: MAX_CANVAS_SIDE, description: "Height in pixels (default unchanged)." },
2645
+ dpi: { type: "number", minimum: 1, maximum: 1e4, description: "Pixels per inch (default unchanged)." }
2646
+ },
2647
+ additionalProperties: false
2648
+ },
2649
+ annotations: { title: "Resize the canvas", ...SET },
2650
+ handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
2651
+ });
2652
+ add({
2653
+ name: "list_papers",
2654
+ title: "List papers",
2655
+ description: "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.",
2656
+ inputSchema: EMPTY,
2657
+ annotations: { title: "List papers", ...READ },
2658
+ handler: () => ({
2659
+ 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 })),
2660
+ families: [...PAPER_FAMILIES],
2661
+ patterns: [...GRAIN_PATTERNS]
2662
+ })
2663
+ });
2664
+ add({
2665
+ name: "get_paper",
2666
+ title: "Get the paper",
2667
+ description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness).",
2668
+ inputSchema: EMPTY,
2669
+ annotations: { title: "Get the paper", ...READ },
2670
+ handler: () => JSON.parse(JSON.stringify(c.document.paper))
2671
+ });
2672
+ const tuning2 = {
2673
+ type: "object",
2674
+ properties: {
2675
+ contrast: { type: "number", minimum: 0, maximum: 3, description: "Height contrast around the middle (default 1)." },
2676
+ invert: { type: "boolean", description: "Swap peaks and valleys." }
2677
+ },
2678
+ additionalProperties: false
2679
+ };
2680
+ add({
2681
+ name: "set_paper",
2682
+ title: "Set the paper",
2683
+ description: "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.",
2684
+ inputSchema: {
2685
+ type: "object",
2686
+ properties: {
2687
+ preset: { type: "string", description: "Paper id from list_papers (its settings, then the other fields here on top)." },
2688
+ color: { ...COLOR, description: "Paper colour #rrggbb." },
2689
+ depth: { type: "number", minimum: 0, maximum: 1, description: "Grain depth 0..1 (0: brushes ignore the grain)." },
2690
+ scale: { type: "number", minimum: 0.25, maximum: 8, description: "Grain scale: 1 natural, 2 twice as coarse." },
2691
+ seed: { type: "integer", minimum: 0, description: "Grain seed: another sheet of the same paper." },
2692
+ pattern: { type: "string", enum: [...GRAIN_PATTERNS], description: "Grain pattern." },
2693
+ tiling: { type: "string", enum: ["repeat", "mirror"], description: "How the grain tile repeats (mirror hides seams)." },
2694
+ shading: { type: "number", minimum: 0, maximum: 1, description: "How much the grain shades the paper colour." },
2695
+ dry: { ...tuning2, description: "Height map tuning for dry media." },
2696
+ wet: { ...tuning2, description: "Height map tuning for wet media (stored for later)." },
2697
+ absorption: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how fast the paper takes water (stored for later)." },
2698
+ permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how far water spreads in the paper (stored for later)." },
2699
+ roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain catches pigment (stored for later)." },
2700
+ name: { type: "string", minLength: 1, description: "Display name." }
2701
+ },
2702
+ additionalProperties: false
2703
+ },
2704
+ annotations: { title: "Set the paper", ...SET },
2705
+ handler: (a) => c.setPaper(a)
2706
+ });
2707
+ add({
2708
+ name: "list_layers",
2709
+ title: "List layers",
2710
+ description: "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.",
2711
+ inputSchema: EMPTY,
2712
+ annotations: { title: "List layers", ...READ },
2713
+ handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
2714
+ });
2715
+ add({
2716
+ name: "add_layer",
2717
+ title: "Add a layer",
2718
+ 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.",
2719
+ inputSchema: {
2720
+ type: "object",
2721
+ properties: {
2722
+ name: { type: "string", minLength: 1, description: 'Layer name (default "Layer n").' },
2723
+ type: { type: "string", enum: ["pixel", "group"], description: "pixel (default) or group." },
2724
+ aboveId: { type: "string", description: "Put it directly above this layer (default the active layer)." },
2725
+ parentId: { type: "string", description: "Put it at the top of this group instead." }
2726
+ },
2727
+ additionalProperties: false
2728
+ },
2729
+ annotations: { title: "Add a layer", ...WRITE },
2730
+ handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
2731
+ });
2732
+ add({
2733
+ name: "update_layer",
2734
+ title: "Change a layer",
2735
+ description: "Changes a layer: name, visibility, opacity, blend mode, clipping to the layer below, locks. One undo step. Returns the layer.",
2736
+ inputSchema: {
2737
+ type: "object",
2738
+ properties: {
2739
+ layerId: { type: "string", description: "Layer id from list_layers." },
2740
+ name: { type: "string", minLength: 1, description: "New name." },
2741
+ visible: { type: "boolean", description: "Show or hide." },
2742
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1." },
2743
+ blend: { type: "string", enum: [..._chunkCRMAXIUVcjs.BLEND_MODES.map((m) => m.id), "pass-through"], description: "Blend mode (pass-through for groups only)." },
2744
+ clipped: { type: "boolean", description: "Clip to the layer below (paint only shows where it has paint)." },
2745
+ locks: {
2746
+ type: "object",
2747
+ description: "Locks to change (others keep their values).",
2748
+ properties: {
2749
+ pixels: { type: "boolean", description: "Refuse painting." },
2750
+ transparency: { type: "boolean", description: "Paint only where the layer already has paint." },
2751
+ position: { type: "boolean", description: "Refuse moving." },
2752
+ all: { type: "boolean", description: "All of the above, plus property changes and removal." }
2753
+ },
2754
+ additionalProperties: false
2755
+ }
2756
+ },
2757
+ required: ["layerId"],
2758
+ additionalProperties: false
2759
+ },
2760
+ annotations: { title: "Change a layer", ...SET },
2761
+ handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
2762
+ });
2763
+ add({
2764
+ name: "set_active_layer",
2765
+ title: "Choose the active layer",
2766
+ description: "Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.",
2767
+ inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2768
+ annotations: { title: "Choose the active layer", ...SET },
2769
+ handler: ({ layerId }) => {
2770
+ c.setActiveLayer(layerId);
2771
+ return c.describeLayer(c.getLayer(layerId));
2772
+ }
2773
+ });
2774
+ add({
2775
+ name: "remove_layer",
2776
+ title: "Remove a layer",
2777
+ description: "Removes a layer (a group with its children). One undo step. Returns the removed id and the new active layer.",
2778
+ inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2779
+ annotations: { title: "Remove a layer", ...DESTRUCTIVE },
2780
+ handler: ({ layerId }) => c.removeLayer(layerId)
2781
+ });
2782
+ add({
2783
+ name: "duplicate_layer",
2784
+ title: "Duplicate a layer",
2785
+ description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
2786
+ inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2787
+ annotations: { title: "Duplicate a layer", ...WRITE },
2788
+ handler: ({ layerId }) => c.duplicateLayer(layerId)
2789
+ });
2790
+ add({
2791
+ name: "move_layer",
2792
+ title: "Reorder a layer",
2793
+ 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.",
2794
+ inputSchema: {
2795
+ type: "object",
2796
+ properties: {
2797
+ layerId: { type: "string", description: "Layer id from list_layers." },
2798
+ index: { type: "integer", minimum: 0, description: "New position among its siblings, 0 = bottom." },
2799
+ parentId: { type: ["string", "null"], description: "Group id to move into, or null for the top level (default its current parent)." }
2800
+ },
2801
+ required: ["layerId", "index"],
2802
+ additionalProperties: false
2803
+ },
2804
+ annotations: { title: "Reorder a layer", ...WRITE },
2805
+ handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
2806
+ });
2807
+ add({
2808
+ name: "merge_layer_down",
2809
+ title: "Merge a layer down",
2810
+ 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.",
2811
+ inputSchema: { type: "object", properties: { layerId: { type: "string", description: "The upper layer, from list_layers." } }, required: ["layerId"], additionalProperties: false },
2812
+ annotations: { title: "Merge a layer down", ...DESTRUCTIVE },
2813
+ handler: ({ layerId }) => c.mergeDown(layerId)
2814
+ });
2815
+ add({
2816
+ name: "fill_layer",
2817
+ title: "Fill a layer",
2818
+ description: "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.",
2819
+ inputSchema: {
2820
+ type: "object",
2821
+ properties: {
2822
+ layerId: LAYER_ID,
2823
+ color: { ...COLOR, description: "Fill colour #rrggbb (default the paint colour)." },
2824
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
2825
+ grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
2826
+ pressure: { type: "number", minimum: 0, maximum: 1, description: "With grain: how far into the valleys, 0..1 (default 0.5)." },
2827
+ blend: { type: "string", enum: _chunkCRMAXIUVcjs.BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal; pigment mixes like paint)." }
2828
+ },
2829
+ additionalProperties: false
2830
+ },
2831
+ annotations: { title: "Fill a layer", ...WRITE },
2832
+ handler: (a) => c.fillLayer(a)
2833
+ });
2834
+ add({
2835
+ name: "clear_layer",
2836
+ title: "Clear a layer",
2837
+ 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).",
2838
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID }, additionalProperties: false },
2839
+ annotations: { title: "Clear a layer", ...DESTRUCTIVE },
2840
+ handler: ({ layerId }) => c.clearLayer(layerId)
2841
+ });
2842
+ add({
2843
+ name: "list_brushes",
2844
+ title: "List brushes",
2845
+ description: "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.",
2846
+ inputSchema: EMPTY,
2847
+ annotations: { title: "List brushes", ...READ },
2848
+ handler: () => {
2849
+ const s = c.getState();
2850
+ return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(_optionalChain([c, 'access', _67 => _67.getBrush, 'call', _68 => _68(s.brush.id), 'optionalAccess', _69 => _69.builtin])), brushes: s.brushes.map(brushSummary) };
2851
+ }
2852
+ });
2853
+ add({
2854
+ name: "get_brush",
2855
+ title: "Get a brush",
2856
+ description: "Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer). Without id, the active brush as edited.",
2857
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes (default the active brush)." } }, additionalProperties: false },
2858
+ annotations: { title: "Get a brush", ...READ },
2859
+ handler: ({ id }) => {
2860
+ if (id === void 0) return brushToJSON(c.getState().brush);
2861
+ const b = c.getBrush(id);
2862
+ if (!b) throw new Error(`Unknown brush "${id}". Call list_brushes for the ids.`);
2863
+ return { ...brushToJSON(b), builtin: Boolean(b.builtin) };
2864
+ }
2865
+ });
2866
+ add({
2867
+ name: "set_brush",
2868
+ title: "Set the brush",
2869
+ description: "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.",
2870
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes to start from." }, ...BRUSH_FIELDS }, additionalProperties: false },
2871
+ annotations: { title: "Set the brush", ...SET },
2872
+ handler: (a) => brushToJSON(c.setBrush(a))
2873
+ });
2874
+ add({
2875
+ name: "save_brush",
2876
+ title: "Save a brush",
2877
+ 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.",
2878
+ inputSchema: {
2879
+ type: "object",
2880
+ properties: {
2881
+ id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,63}$", description: "Library id: lowercase letters, digits and dashes." },
2882
+ name: { type: "string", minLength: 1, maxLength: 80, description: "Display name (default the brush name)." }
2883
+ },
2884
+ required: ["id"],
2885
+ additionalProperties: false
2886
+ },
2887
+ annotations: { title: "Save a brush", ...SET },
2888
+ handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name }))
2889
+ });
2890
+ add({
2891
+ name: "remove_brush",
2892
+ title: "Remove a saved brush",
2893
+ description: "Removes a user brush from the library (built-in brushes cannot be removed). Library state, not an undo step. Returns the removed id.",
2894
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "User brush id from list_brushes." } }, required: ["id"], additionalProperties: false },
2895
+ annotations: { title: "Remove a saved brush", ...DESTRUCTIVE },
2896
+ handler: ({ id }) => {
2897
+ c.removeBrush(id);
2898
+ return { removed: id };
2899
+ }
2900
+ });
2901
+ add({
2902
+ name: "export_brushes",
2903
+ title: "Export brushes",
2904
+ description: "Returns a brush library file (JSON: format pixerate-brushes, version 1, brushes) of the given brushes, or of every user brush. Changes nothing.",
2905
+ inputSchema: {
2906
+ type: "object",
2907
+ properties: { ids: { type: "array", items: { type: "string", description: "Brush id." }, description: "Brush ids from list_brushes (default every user brush)." } },
2908
+ additionalProperties: false
2909
+ },
2910
+ annotations: { title: "Export brushes", ...READ },
2911
+ handler: ({ ids }) => c.exportBrushes(ids)
2912
+ });
2913
+ add({
2914
+ name: "import_brushes",
2915
+ title: "Import brushes",
2916
+ description: "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.",
2917
+ inputSchema: {
2918
+ type: "object",
2919
+ properties: {
2920
+ library: { type: "object", description: 'The library file: {format: "pixerate-brushes", version: 1, brushes: [...]}.' },
2921
+ replace: { type: "boolean", description: "Replace user brushes with the same id (default false)." }
2922
+ },
2923
+ required: ["library"],
2924
+ additionalProperties: false
2925
+ },
2926
+ annotations: { title: "Import brushes", ...WRITE },
2927
+ handler: ({ library, replace }) => c.importBrushes(library, { replace })
2928
+ });
2929
+ add({
2930
+ name: "render_brush_thumbnail",
2931
+ title: "Render a brush thumbnail",
2932
+ 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.",
2933
+ inputSchema: {
2934
+ type: "object",
2935
+ properties: {
2936
+ id: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
2937
+ width: { type: "integer", minimum: 16, maximum: 512, description: "Width in pixels (default 160)." },
2938
+ height: { type: "integer", minimum: 16, maximum: 512, description: "Height in pixels (default 48)." },
2939
+ color: { ...COLOR, description: "Stroke colour (default the paint colour)." }
2940
+ },
2941
+ additionalProperties: false
2942
+ },
2943
+ annotations: { title: "Render a brush thumbnail", ...READ },
2944
+ handler: ({ id, width, height, color }) => {
2945
+ const t = c.brushThumbnail(_nullishCoalesce(id, () => ( c.getState().brush.id)), { width, height, color });
2946
+ return {
2947
+ content: [
2948
+ { type: "image", data: _chunkWSTEXEKDcjs.bytesToBase64.call(void 0, _chunkWSTEXEKDcjs.encodePng.call(void 0, t.width, t.height, t.data)), mimeType: "image/png" },
2949
+ { type: "text", text: `Brush ${_nullishCoalesce(id, () => ( c.getState().brush.id))} ${t.width}\xD7${t.height}` }
2950
+ ]
2951
+ };
2952
+ }
2953
+ });
2954
+ add({
2955
+ name: "set_color",
2956
+ title: "Set the paint colour",
2957
+ 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.",
2958
+ inputSchema: {
2959
+ type: "object",
2960
+ properties: { color: { ...COLOR, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR, description: "Second colour #rrggbb." } },
2961
+ additionalProperties: false
2962
+ },
2963
+ annotations: { title: "Set the paint colour", ...SET },
2964
+ handler: ({ color, secondaryColor }) => {
2965
+ if (color !== void 0) c.setColor(color);
2966
+ if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
2967
+ const s = c.getState();
2968
+ return { color: s.color, secondaryColor: s.secondaryColor };
2969
+ }
2970
+ });
2971
+ add({
2972
+ name: "pick_color",
2973
+ title: "Pick a colour",
2974
+ 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.",
2975
+ inputSchema: {
2976
+ type: "object",
2977
+ properties: {
2978
+ x: { type: "number", description: "Document x in pixels." },
2979
+ y: { type: "number", description: "Document y in pixels." },
2980
+ layerId: { type: "string", description: "Pick from this layer only (default the painting as shown)." }
2981
+ },
2982
+ required: ["x", "y"],
2983
+ additionalProperties: false
2984
+ },
2985
+ annotations: { title: "Pick a colour", ...READ },
2986
+ handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
2987
+ });
2988
+ add({
2989
+ name: "get_mixer_brush",
2990
+ title: "Get the Mixer Brush load",
2991
+ 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.",
2992
+ inputSchema: EMPTY,
2993
+ annotations: { title: "Get the Mixer Brush load", ...READ },
2994
+ handler: () => loadResult(c.getState().mixerLoad)
2995
+ });
2996
+ add({
2997
+ name: "load_mixer_brush",
2998
+ title: "Load the Mixer Brush",
2999
+ 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.",
3000
+ inputSchema: { type: "object", properties: { color: { ...COLOR, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
3001
+ annotations: { title: "Load the Mixer Brush", ...SET },
3002
+ handler: ({ color }) => loadResult(c.loadMixerBrush(color))
3003
+ });
3004
+ add({
3005
+ name: "clean_mixer_brush",
3006
+ title: "Clean the Mixer Brush",
3007
+ description: "Empties the Mixer Brush pickup well (and with emptyReservoir its paint too). Editor state, not an undo step. Returns the load.",
3008
+ inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
3009
+ annotations: { title: "Clean the Mixer Brush", ...SET },
3010
+ handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
3011
+ });
3012
+ add({
3013
+ name: "paint_stroke",
3014
+ title: "Paint a brush stroke",
3015
+ description: "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.",
3016
+ inputSchema: {
3017
+ type: "object",
3018
+ properties: {
3019
+ points: POINTS,
3020
+ layerId: LAYER_ID,
3021
+ brushId: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
3022
+ brush: { type: "object", description: "Brush changes for this stroke only.", properties: BRUSH_FIELDS, additionalProperties: false },
3023
+ color: { ...COLOR, description: "Paint colour #rrggbb (default the paint colour)." },
3024
+ seed: { type: "integer", minimum: 0, description: "Jitter seed (default a new one); the same seed and points give the same marks." }
3025
+ },
3026
+ required: ["points"],
3027
+ additionalProperties: false
3028
+ },
3029
+ annotations: { title: "Paint a brush stroke", ...WRITE },
3030
+ handler: (a) => {
3031
+ const r = c.paintStroke({ points: a.points, layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, seed: a.seed });
3032
+ return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
3033
+ }
3034
+ });
3035
+ add({
3036
+ name: "list_strokes",
3037
+ title: "List recorded strokes",
3038
+ description: "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.",
3039
+ inputSchema: {
3040
+ type: "object",
3041
+ properties: {
3042
+ ids: { type: "array", items: { type: "string", description: "Stroke id." }, description: "Only these strokes, with their points." },
3043
+ last: { type: "integer", minimum: 1, maximum: 1e3, description: "Only the most recent n strokes (default 100)." }
3044
+ },
3045
+ additionalProperties: false
3046
+ },
3047
+ annotations: { title: "List recorded strokes", ...READ },
3048
+ handler: ({ ids, last }) => {
3049
+ const all = c.getStrokes();
3050
+ const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(_nullishCoalesce(last, () => ( 100))));
3051
+ if (ids) {
3052
+ 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.`);
3053
+ }
3054
+ return {
3055
+ total: all.length,
3056
+ strokes: picked.map((s) => {
3057
+ const xs = s.points.map((p) => p.x);
3058
+ const ys = s.points.map((p) => p.y);
3059
+ const pad = s.brush.tip.size / 2;
3060
+ return {
3061
+ id: s.id,
3062
+ layerId: s.layerId,
3063
+ brushId: s.brush.id,
3064
+ brushName: s.brush.name,
3065
+ color: s.color,
3066
+ points: ids ? s.points : s.points.length,
3067
+ seed: s.seed,
3068
+ copies: _nullishCoalesce(_optionalChain([s, 'access', _70 => _70.copies, 'optionalAccess', _71 => _71.length]), () => ( 0)),
3069
+ 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) }
3070
+ };
3071
+ })
3072
+ };
3073
+ }
3074
+ });
3075
+ add({
3076
+ name: "replay_strokes",
3077
+ title: "Replay strokes, varied",
3078
+ description: "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.",
3079
+ inputSchema: {
3080
+ type: "object",
3081
+ properties: {
3082
+ strokeIds: { type: "array", minItems: 1, maxItems: 500, items: { type: "string", description: "Stroke id." }, description: "Stroke ids from list_strokes." },
3083
+ dx: { type: "number", description: "Move right by this many pixels per repeat (default 0)." },
3084
+ dy: { type: "number", description: "Move down by this many pixels per repeat (default 0)." },
3085
+ scale: { type: "number", exclusiveMinimum: 0, maximum: 20, description: "Scale per repeat about the origin; brush size scales too (default 1)." },
3086
+ rotate: { type: "number", minimum: -360, maximum: 360, description: "Rotate per repeat about the origin, degrees clockwise (default 0)." },
3087
+ flipX: { type: "boolean", description: "Mirror left to right about the origin (default false)." },
3088
+ origin: {
3089
+ type: "object",
3090
+ description: "Centre for scale, rotate and flip (default the centre of the strokes).",
3091
+ properties: { x: { type: "number", description: "Document x." }, y: { type: "number", description: "Document y." } },
3092
+ required: ["x", "y"],
3093
+ additionalProperties: false
3094
+ },
3095
+ pressureScale: { type: "number", minimum: 0, maximum: 4, description: "Multiply every pressure (default 1)." },
3096
+ color: { ...COLOR, description: "Recolour (default each stroke\u2019s colour)." },
3097
+ brushId: { type: "string", description: "Replay with this brush from list_brushes (default each stroke\u2019s brush)." },
3098
+ layerId: { type: "string", description: "Paint on this layer (default each stroke\u2019s layer)." },
3099
+ repeat: { type: "integer", minimum: 1, maximum: 100, description: "How many copies (default 1)." }
3100
+ },
3101
+ required: ["strokeIds"],
3102
+ additionalProperties: false
3103
+ },
3104
+ annotations: { title: "Replay strokes, varied", ...WRITE },
3105
+ handler: (a) => c.replayStrokes(a)
3106
+ });
3107
+ add({
3108
+ name: "set_view",
3109
+ title: "Rotate, mirror or zoom the view",
3110
+ 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.",
3111
+ inputSchema: {
3112
+ type: "object",
3113
+ properties: {
3114
+ zoom: { type: "number", minimum: MIN_ZOOM, maximum: MAX_ZOOM, description: "Screen pixels per document pixel." },
3115
+ rotation: { type: "number", minimum: -360, maximum: 360, description: "Rotation in degrees clockwise." },
3116
+ mirror: { type: "boolean", description: "Mirror left to right." },
3117
+ reset: { type: "boolean", description: "Back to 100 %, no rotation, no mirror, centred (applied first)." }
3118
+ },
3119
+ additionalProperties: false
3120
+ },
3121
+ annotations: { title: "Rotate, mirror or zoom the view", ...SET },
3122
+ handler: ({ reset, ...v }) => {
3123
+ if (reset) c.resetView();
3124
+ return c.setView(v);
3125
+ }
3126
+ });
3127
+ add({
3128
+ name: "set_hand",
3129
+ title: "Set the drawing hand",
3130
+ 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.",
3131
+ inputSchema: { type: "object", properties: { hand: { type: "string", enum: ["right", "left"], description: "right or left." } }, required: ["hand"], additionalProperties: false },
3132
+ annotations: { title: "Set the drawing hand", ...SET },
3133
+ handler: ({ hand }) => ({ hand: c.setHand(hand) })
3134
+ });
3135
+ add({
3136
+ name: "undo_paint",
3137
+ title: "Undo",
3138
+ 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.",
3139
+ inputSchema: EMPTY,
3140
+ annotations: { title: "Undo", ...WRITE },
3141
+ handler: () => {
3142
+ const undone = c.undo();
3143
+ const s = c.getState();
3144
+ return { undone, nextUndo: s.undoLabel, nextRedo: s.redoLabel };
3145
+ }
3146
+ });
3147
+ add({
3148
+ name: "redo_paint",
3149
+ title: "Redo",
3150
+ description: "Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.",
3151
+ inputSchema: EMPTY,
3152
+ annotations: { title: "Redo", ...WRITE },
3153
+ handler: () => {
3154
+ const redone = c.redo();
3155
+ const s = c.getState();
3156
+ return { redone, nextUndo: s.undoLabel, nextRedo: s.redoLabel };
3157
+ }
3158
+ });
3159
+ add({
3160
+ name: "render_preview",
3161
+ title: "Render a preview",
3162
+ 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.",
3163
+ inputSchema: {
3164
+ type: "object",
3165
+ properties: {
3166
+ maxSide: { type: "integer", minimum: 16, maximum: 2048, description: "Longest side in pixels (default 512)." },
3167
+ layerIds: { type: "array", items: { type: "string", description: "Layer id." }, description: "Only these layers (default all)." },
3168
+ paper: { type: "boolean", description: "Include the paper (default true)." }
3169
+ },
3170
+ additionalProperties: false
3171
+ },
3172
+ annotations: { title: "Render a preview", ...READ },
3173
+ handler: ({ maxSide, layerIds, paper }) => {
3174
+ if (layerIds) {
3175
+ for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
3176
+ }
3177
+ const p = c.previewPngBase64({ maxSide: _nullishCoalesce(maxSide, () => ( 512)), layerIds, paper });
3178
+ return { content: [{ type: "image", data: p.data, mimeType: "image/png" }, { type: "text", text: `Preview ${p.width}\xD7${p.height}` }] };
3179
+ }
3180
+ });
3181
+ add({
3182
+ name: "export_image",
3183
+ title: "Export the painting",
3184
+ 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.",
3185
+ inputSchema: {
3186
+ type: "object",
3187
+ properties: {
3188
+ format: { type: "string", enum: ["image/png", "image/jpeg", "image/webp"], description: "File type (default image/png)." },
3189
+ quality: { type: "number", minimum: 0, maximum: 1, description: "JPEG / WebP quality 0..1 (default 0.92)." },
3190
+ scale: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Shrink by this factor (default 1)." },
3191
+ paper: { type: "boolean", description: "Include the paper (default true)." }
3192
+ },
3193
+ additionalProperties: false
3194
+ },
3195
+ annotations: { title: "Export the painting", ...READ },
3196
+ handler: async ({ format, quality, scale, paper }) => {
3197
+ const bytes = await c.exportImage({ format, quality, scale, paper });
3198
+ return { mimeType: _nullishCoalesce(format, () => ( "image/png")), bytes: bytes.length, data: _chunkWSTEXEKDcjs.bytesToBase64.call(void 0, bytes) };
3199
+ }
3200
+ });
3201
+ add({
3202
+ name: "export_psd",
3203
+ title: "Export as PSD",
3204
+ description: '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).',
3205
+ inputSchema: EMPTY,
3206
+ annotations: { title: "Export as PSD", ...READ },
3207
+ handler: () => {
3208
+ const r = c.exportPsd();
3209
+ return { data: _chunkWSTEXEKDcjs.bytesToBase64.call(void 0, r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
3210
+ }
3211
+ });
3212
+ add({
3213
+ name: "save_document",
3214
+ title: "Save the document",
3215
+ 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.",
3216
+ inputSchema: { type: "object", properties: { strokes: { type: "boolean", description: "Include the recorded strokes (default true)." } }, additionalProperties: false },
3217
+ annotations: { title: "Save the document", ...READ },
3218
+ handler: ({ strokes }) => c.toJSON({ strokes })
3219
+ });
3220
+ add({
3221
+ name: "open_document",
3222
+ title: "Open a document",
3223
+ 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.",
3224
+ inputSchema: { type: "object", properties: { document: { type: "object", description: "The saved document JSON." } }, required: ["document"], additionalProperties: false },
3225
+ annotations: { title: "Open a document", ...DESTRUCTIVE },
3226
+ handler: ({ document }) => {
3227
+ const problems = c.loadJSON(document);
3228
+ const d = c.document;
3229
+ return { width: d.canvas.width, height: d.canvas.height, layers: c.listLayers(), strokes: d.strokes.length, problems };
3230
+ }
3231
+ });
3232
+ tools.push(..._chunkPV42JITTcjs.createPigmentTools.call(void 0, ));
3233
+ return { id: _nullishCoalesce(options.id, () => ( "paint-editor")), namespace: options.namespace, tools };
3234
+ }
3235
+
3236
+
3237
+
3238
+
3239
+
3240
+
3241
+
3242
+
3243
+
3244
+
3245
+
3246
+
3247
+
3248
+
3249
+
3250
+
3251
+
3252
+
3253
+
3254
+
3255
+
3256
+
3257
+
3258
+
3259
+
3260
+
3261
+
3262
+
3263
+
3264
+
3265
+
3266
+
3267
+
3268
+
3269
+
3270
+
3271
+
3272
+
3273
+
3274
+
3275
+
3276
+
3277
+
3278
+
3279
+
3280
+
3281
+
3282
+
3283
+
3284
+
3285
+
3286
+
3287
+
3288
+
3289
+
3290
+
3291
+
3292
+
3293
+
3294
+
3295
+
3296
+
3297
+
3298
+
3299
+
3300
+
3301
+
3302
+
3303
+
3304
+
3305
+
3306
+
3307
+
3308
+
3309
+
3310
+
3311
+
3312
+
3313
+
3314
+
3315
+
3316
+
3317
+
3318
+
3319
+
3320
+
3321
+
3322
+
3323
+
3324
+
3325
+
3326
+
3327
+
3328
+
3329
+ exports.BRUSH_ID = BRUSH_ID; exports.BRUSH_ROTATIONS = BRUSH_ROTATIONS; exports.BUILTIN_BRUSHES = BUILTIN_BRUSHES; exports.DEFAULT_BRUSH_ID = DEFAULT_BRUSH_ID; exports.DEFAULT_DPI = DEFAULT_DPI; exports.DEFAULT_DYNAMICS = DEFAULT_DYNAMICS; exports.DEFAULT_PAPER = DEFAULT_PAPER; exports.DEFAULT_VIEW = DEFAULT_VIEW; exports.DryStroke = DryStroke; exports.GRAIN_PATTERNS = GRAIN_PATTERNS; exports.GRAIN_TILE = GRAIN_TILE; exports.IDENTITY_AFFINE = IDENTITY_AFFINE; exports.IDENTITY_COPY = IDENTITY_COPY; exports.MAX_BRUSH_SIZE = _chunkPV42JITTcjs.MAX_BRUSH_SIZE; exports.MAX_CANVAS_SIDE = MAX_CANVAS_SIDE; exports.MAX_ZOOM = MAX_ZOOM; exports.MIN_CANVAS_SIDE = MIN_CANVAS_SIDE; exports.MIN_ZOOM = MIN_ZOOM; exports.PAINT_BRUSH_CATEGORIES = PAINT_BRUSH_CATEGORIES; exports.PAINT_BRUSH_ENGINES = PAINT_BRUSH_ENGINES; exports.PAINT_FORMAT = PAINT_FORMAT; exports.PAINT_SCHEMA_VERSION = PAINT_SCHEMA_VERSION; exports.PAPER_FAMILIES = PAPER_FAMILIES; exports.PAPER_LIBRARY = PAPER_LIBRARY; exports.PAPER_NODE_ID = PAPER_NODE_ID; exports.PaintDocumentError = PaintDocumentError; exports.PaintEditorController = PaintEditorController; exports.PaintEditorError = PaintEditorError; exports.PaintStrokeSession = PaintStrokeSession; exports.PaperGrain = PaperGrain; exports.PenDabWalker = PenDabWalker; exports.PenSmoother = PenSmoother; exports.TRACING_NODE_ID = TRACING_NODE_ID; exports.TileableNoise = TileableNoise; exports.affineCopy = affineCopy; exports.applyAffine = applyAffine; exports.brushToJSON = brushToJSON; exports.clonePaper = clonePaper; exports.composeAffine = composeAffine; exports.createPaintCompositor = createPaintCompositor; exports.createPaintDocument = createPaintDocument; exports.createPaintEditorToolProvider = createPaintEditorToolProvider; exports.createPaintLayer = createPaintLayer; exports.defaultImageEncoder = defaultImageEncoder; exports.deserializePaintDocument = deserializePaintDocument; exports.deserializePaintDocumentAsync = deserializePaintDocumentAsync; exports.documentExtensions = documentExtensions; exports.documentToScreen = documentToScreen; exports.downsampleRgba = downsampleRgba; exports.fitZoom = fitZoom; exports.grainCatch = grainCatch; exports.grainHeights = grainHeights; exports.hash2 = hash2; exports.hexToRgb8 = hexToRgb8; exports.invertAffine = invertAffine; exports.isPalmTouch = isPalmTouch; exports.mapPenPoint = mapPenPoint; exports.mix32 = mix32; exports.mixerLoadForStroke = mixerLoadForStroke; exports.mixerTip = mixerTip; exports.newPaintLayerId = newPaintLayerId; exports.normalizeBrushStroke = normalizeBrushStroke; exports.normalizeCanvas = normalizeCanvas; exports.normalizeDynamics = normalizeDynamics; exports.normalizePaintBrush = normalizePaintBrush; exports.normalizePaper = normalizePaper; exports.normalizeRotation = normalizeRotation; exports.normalizeView = normalizeView; exports.paperGrain = paperGrain; exports.paperKey = paperKey; exports.paperNode = paperNode; exports.paperPreset = paperPreset; exports.parseBrushLibrary = parseBrushLibrary; exports.penOrientation = penOrientation; exports.pointerToPenPoint = pointerToPenPoint; exports.rand01 = rand01; exports.randSigned = randSigned; exports.renderBrushThumbnail = renderBrushThumbnail; exports.renderPaper = renderPaper; exports.samplePressure = samplePressure; exports.screenToDocument = screenToDocument; exports.seedFromString = seedFromString; exports.serializePaintDocument = serializePaintDocument; exports.serializePaintDocumentExternal = serializePaintDocumentExternal; exports.strokeBounds = strokeBounds; exports.thumbnailStroke = thumbnailStroke; exports.tiltToOrientation = tiltToOrientation; exports.toStrokePoint = toStrokePoint; exports.variationAffine = variationAffine; exports.varyStroke = varyStroke; exports.viewCssTransform = viewCssTransform; exports.viewMatrix = viewMatrix; exports.viewPenToDocument = viewPenToDocument;
3330
+ //# sourceMappingURL=index.cjs.map