@pixerate/editor 0.103.0 → 0.105.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 (34) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -0
  2. package/dist/{chunk-NQLQ2JOI.js → chunk-4X5MIVCE.js} +1155 -295
  3. package/dist/chunk-4X5MIVCE.js.map +1 -0
  4. package/dist/{chunk-VSMM7B3P.cjs → chunk-ZID226ZG.cjs} +1169 -309
  5. package/dist/chunk-ZID226ZG.cjs.map +1 -0
  6. package/dist/{compositor-CTMU-B1t.d.cts → compositor-Bc3PNP3C.d.cts} +1 -1
  7. package/dist/{compositor-B1ZrKR89.d.ts → compositor-BsYYllvR.d.ts} +1 -1
  8. package/dist/{export-X9rY79p0.d.cts → export-_m3Es4ru.d.cts} +19 -4
  9. package/dist/{export-BLpMyCYg.d.ts → export-iMrop3Jm.d.ts} +19 -4
  10. package/dist/image-editor/index.cjs +523 -539
  11. package/dist/image-editor/index.cjs.map +1 -1
  12. package/dist/image-editor/index.d.cts +16 -8
  13. package/dist/image-editor/index.d.ts +16 -8
  14. package/dist/image-editor/index.js +37 -53
  15. package/dist/image-editor/index.js.map +1 -1
  16. package/dist/{mixer-DGcH6HLu.d.cts → mixer-C1M1Nua_.d.cts} +1 -1
  17. package/dist/{mixer-DG_9Yu2W.d.ts → mixer-Cu5a9Bzd.d.ts} +1 -1
  18. package/dist/paint/index.d.cts +2 -2
  19. package/dist/paint/index.d.ts +2 -2
  20. package/dist/paint-editor/index.cjs +1531 -128
  21. package/dist/paint-editor/index.cjs.map +1 -1
  22. package/dist/paint-editor/index.d.cts +461 -14
  23. package/dist/paint-editor/index.d.ts +461 -14
  24. package/dist/paint-editor/index.js +1479 -76
  25. package/dist/paint-editor/index.js.map +1 -1
  26. package/dist/raster/index.d.cts +4 -4
  27. package/dist/raster/index.d.ts +4 -4
  28. package/dist/{surface-AfHCH7BR.d.cts → surface-BBJN9wxg.d.cts} +1 -1
  29. package/dist/{surface-AfHCH7BR.d.ts → surface-BBJN9wxg.d.ts} +1 -1
  30. package/package.json +1 -1
  31. package/skills/pixerate-image-editor/SKILL.md +14 -5
  32. package/skills/pixerate-paint-editor/SKILL.md +49 -8
  33. package/dist/chunk-NQLQ2JOI.js.map +0 -1
  34. package/dist/chunk-VSMM7B3P.cjs.map +0 -1
@@ -4,16 +4,19 @@ import {
4
4
  resolveExternalAssets,
5
5
  serializeLayers,
6
6
  serializeLayersExternal
7
- } from "../chunk-NQLQ2JOI.js";
7
+ } from "../chunk-4X5MIVCE.js";
8
8
  import {
9
9
  RasterCompositor,
10
10
  bytesToBase64,
11
+ decodePng,
11
12
  encodePng,
12
13
  findLayer,
13
14
  flattenLayers,
14
15
  insertLayer,
15
16
  isLayerLocked,
16
- mapLayer
17
+ mapLayer,
18
+ parseDataUrl,
19
+ pngDataUrl
17
20
  } from "../chunk-67FRIOPZ.js";
18
21
  import "../chunk-GX332W2C.js";
19
22
  import {
@@ -43,6 +46,7 @@ import {
43
46
  finishTile,
44
47
  isBlendMode,
45
48
  readPixel,
49
+ readRegion,
46
50
  surfaceFromPixels,
47
51
  tileKey,
48
52
  tilesInRect
@@ -1010,6 +1014,8 @@ var PaintStrokeSession = class {
1010
1014
  /** Adds raw pen samples (document pixels). */
1011
1015
  push(points) {
1012
1016
  if (this.closed || !points.length) return;
1017
+ const filter = this.init.inputFilter;
1018
+ if (filter) points = points.map(filter);
1013
1019
  for (const p of points) this.points.push(p);
1014
1020
  this.lastRaw = points[points.length - 1];
1015
1021
  if (this.dry) this.update(this.dry.push(points));
@@ -1307,7 +1313,7 @@ function downsampleRgba(data, width, height, tw, th) {
1307
1313
  // src/paint-editor/serialize.ts
1308
1314
  var PAINT_FORMAT = "pixerate-paint";
1309
1315
  var PAINT_SCHEMA_VERSION = 1;
1310
- var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes"]);
1316
+ var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches"]);
1311
1317
  function documentExtensions(raw) {
1312
1318
  const out = {};
1313
1319
  for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;
@@ -1325,6 +1331,8 @@ function header(state, options) {
1325
1331
  activeLayerId: state.activeLayerId
1326
1332
  };
1327
1333
  if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));
1334
+ if (options.guides !== void 0) out.guides = JSON.parse(JSON.stringify(options.guides));
1335
+ if (options.swatches !== void 0) out.swatches = JSON.parse(JSON.stringify(options.swatches));
1328
1336
  return out;
1329
1337
  }
1330
1338
  function serializePaintDocument(state, options = {}) {
@@ -1372,7 +1380,7 @@ function deserializePaintDocument(raw) {
1372
1380
  const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);
1373
1381
  const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
1374
1382
  const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : flattenLayers(layers).filter((l) => l.type === "pixel").pop()?.id ?? null;
1375
- return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), problems };
1383
+ return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, problems };
1376
1384
  }
1377
1385
  async function deserializePaintDocumentAsync(raw, store) {
1378
1386
  if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
@@ -1501,6 +1509,28 @@ function defaultImageEncoder() {
1501
1509
  return new Uint8Array(await blob.arrayBuffer());
1502
1510
  };
1503
1511
  }
1512
+ async function decodeImageBlob(blob) {
1513
+ const g = globalThis;
1514
+ if (typeof g.createImageBitmap !== "function" || typeof g.OffscreenCanvas !== "function") throw new Error("Decoding images needs a browser (createImageBitmap and OffscreenCanvas).");
1515
+ const bitmap = await g.createImageBitmap(blob);
1516
+ const canvas = new g.OffscreenCanvas(bitmap.width, bitmap.height);
1517
+ const ctx = canvas.getContext("2d");
1518
+ if (!ctx) throw new Error("No 2D context to decode with.");
1519
+ ctx.drawImage(bitmap, 0, 0);
1520
+ const data = new Uint8ClampedArray(ctx.getImageData(0, 0, bitmap.width, bitmap.height).data);
1521
+ bitmap.close?.();
1522
+ return { width: bitmap.width, height: bitmap.height, data };
1523
+ }
1524
+ function downloadBytes(bytes, fileName, mimeType) {
1525
+ const g = globalThis;
1526
+ if (!g.document || !g.URL || !g.Blob) throw new Error("Downloading needs a browser.");
1527
+ const url = g.URL.createObjectURL(new g.Blob([bytes], { type: mimeType }));
1528
+ const a = g.document.createElement("a");
1529
+ a.href = url;
1530
+ a.download = fileName;
1531
+ a.click();
1532
+ setTimeout(() => g.URL.revokeObjectURL(url), 1e3);
1533
+ }
1504
1534
 
1505
1535
  // src/paint-editor/thumbnails.ts
1506
1536
  function thumbnailStroke(width, height) {
@@ -1562,6 +1592,411 @@ function renderBrushThumbnail(brush, options) {
1562
1592
  return { width, height, data };
1563
1593
  }
1564
1594
 
1595
+ // src/paint-editor/guides.ts
1596
+ var SYMMETRY_MODES = ["off", "vertical", "horizontal", "dual", "diagonal", "kaleidoscope"];
1597
+ function defaultGuides(canvas) {
1598
+ return {
1599
+ symmetry: { mode: "off", center: { x: canvas.width / 2, y: canvas.height / 2 }, rotation: 0, segments: 6, mirror: true },
1600
+ perspective: { enabled: false, points: [], snap: true },
1601
+ tracing: null,
1602
+ references: []
1603
+ };
1604
+ }
1605
+ var MAX_COORDINATE = 1e6;
1606
+ var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
1607
+ var coord = (v, d) => finite2(v) && Math.abs(v) <= MAX_COORDINATE ? v : d;
1608
+ function normalizeSymmetry(raw, base) {
1609
+ const r = raw;
1610
+ const c = r.center ?? {};
1611
+ return {
1612
+ mode: SYMMETRY_MODES.includes(r.mode) ? r.mode : base.mode,
1613
+ center: { x: coord(c.x, base.center.x), y: coord(c.y, base.center.y) },
1614
+ rotation: finite2(r.rotation) ? (r.rotation % 360 + 360) % 360 : base.rotation,
1615
+ segments: finite2(r.segments) ? Math.min(32, Math.max(2, Math.round(r.segments))) : base.segments,
1616
+ mirror: typeof r.mirror === "boolean" ? r.mirror : base.mirror
1617
+ };
1618
+ }
1619
+ function reflectAbout(c, a) {
1620
+ const dx = Math.cos(a);
1621
+ const dy = Math.sin(a);
1622
+ const m00 = 2 * dx * dx - 1;
1623
+ const m01 = 2 * dx * dy;
1624
+ const m11 = 2 * dy * dy - 1;
1625
+ return [m00, m01, m01, m11, c.x - (m00 * c.x + m01 * c.y), c.y - (m01 * c.x + m11 * c.y)];
1626
+ }
1627
+ function rotateAbout(c, a) {
1628
+ const cos = Math.cos(a);
1629
+ const sin = Math.sin(a);
1630
+ return [cos, -sin, sin, cos, c.x - (cos * c.x - sin * c.y), c.y - (sin * c.x + cos * c.y)];
1631
+ }
1632
+ var rad = (deg) => deg * Math.PI / 180;
1633
+ function symmetryAxisAngles(g) {
1634
+ if (g.mode === "off") return [];
1635
+ if (g.mode === "kaleidoscope") return Array.from({ length: g.segments }, (_, k) => rad(g.rotation) + k * Math.PI / g.segments);
1636
+ const initial = g.mode === "horizontal" ? 0 : g.mode === "diagonal" ? 45 : 90;
1637
+ const a = rad(initial + g.rotation);
1638
+ return g.mode === "dual" ? [a, a + Math.PI / 2] : [a];
1639
+ }
1640
+ function symmetryCopies(g) {
1641
+ const c = g.center;
1642
+ switch (g.mode) {
1643
+ case "off":
1644
+ return [];
1645
+ case "vertical":
1646
+ case "horizontal":
1647
+ case "diagonal":
1648
+ return symmetryAxisAngles(g).map((a) => reflectAbout(c, a));
1649
+ case "dual": {
1650
+ const [a, b] = symmetryAxisAngles(g);
1651
+ return [reflectAbout(c, a), reflectAbout(c, b), rotateAbout(c, Math.PI)];
1652
+ }
1653
+ case "kaleidoscope": {
1654
+ const out = [];
1655
+ const n = g.segments;
1656
+ for (let k = 1; k < n; k++) out.push(rotateAbout(c, 2 * Math.PI * k / n));
1657
+ if (g.mirror) {
1658
+ const first = reflectAbout(c, rad(g.rotation));
1659
+ for (let k = 0; k < n; k++) out.push(composeAffine(rotateAbout(c, 2 * Math.PI * k / n), first));
1660
+ }
1661
+ return out;
1662
+ }
1663
+ }
1664
+ }
1665
+ function symmetryAxisSegments(g, size) {
1666
+ const out = [];
1667
+ for (const a of symmetryAxisAngles(g)) {
1668
+ const d = [Math.cos(a), Math.sin(a)];
1669
+ const origin = [g.center.x, g.center.y];
1670
+ const extent = [size.width, size.height];
1671
+ let lo = -Infinity;
1672
+ let hi = Infinity;
1673
+ let ok = true;
1674
+ for (let i = 0; i < 2; i++) {
1675
+ if (Math.abs(d[i]) < 1e-12) {
1676
+ if (origin[i] < 0 || origin[i] > extent[i]) ok = false;
1677
+ } else {
1678
+ const t0 = -origin[i] / d[i];
1679
+ const t1 = (extent[i] - origin[i]) / d[i];
1680
+ lo = Math.max(lo, Math.min(t0, t1));
1681
+ hi = Math.min(hi, Math.max(t0, t1));
1682
+ }
1683
+ }
1684
+ if (!ok || !Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo) continue;
1685
+ out.push([{ x: g.center.x + lo * d[0], y: g.center.y + lo * d[1] }, { x: g.center.x + hi * d[0], y: g.center.y + hi * d[1] }]);
1686
+ }
1687
+ return out;
1688
+ }
1689
+ function normalizePerspective(raw, base) {
1690
+ const r = raw;
1691
+ let points = base.points;
1692
+ if (Array.isArray(r.points)) {
1693
+ if (r.points.length > 3) throw new Error(`A perspective guide has one to three vanishing points; got ${r.points.length}.`);
1694
+ points = r.points.map((p, i) => {
1695
+ const q = p ?? {};
1696
+ if (!finite2(q.x) || !finite2(q.y) || Math.abs(q.x) > MAX_COORDINATE || Math.abs(q.y) > MAX_COORDINATE) {
1697
+ throw new Error(`points[${i}] needs finite x and y within \xB1${MAX_COORDINATE} document pixels.`);
1698
+ }
1699
+ return { x: q.x, y: q.y };
1700
+ });
1701
+ }
1702
+ const enabled = typeof r.enabled === "boolean" ? r.enabled : base.enabled;
1703
+ if (enabled && !points.length) throw new Error("An enabled perspective guide needs at least one vanishing point; pass points.");
1704
+ return { enabled, points, snap: typeof r.snap === "boolean" ? r.snap : base.snap };
1705
+ }
1706
+ function perspectiveDirections(g, p) {
1707
+ const out = [];
1708
+ for (const v of g.points) {
1709
+ const dx = v.x - p.x;
1710
+ const dy = v.y - p.y;
1711
+ const len = Math.hypot(dx, dy);
1712
+ if (len > 1e-6) out.push({ x: dx / len, y: dy / len });
1713
+ }
1714
+ if (g.points.length <= 2) out.push({ x: 0, y: 1 });
1715
+ if (g.points.length <= 1) out.push({ x: 1, y: 0 });
1716
+ return out;
1717
+ }
1718
+ var SNAP_DECIDE_DISTANCE = 6;
1719
+ var PerspectiveSnapper = class {
1720
+ constructor(guide) {
1721
+ this.guide = guide;
1722
+ }
1723
+ guide;
1724
+ start = null;
1725
+ dir = null;
1726
+ /** The line the stroke follows, once chosen. */
1727
+ get line() {
1728
+ return this.start && this.dir ? { from: this.start, direction: this.dir } : null;
1729
+ }
1730
+ snap(p) {
1731
+ if (!this.guide.enabled || !this.guide.snap || !this.guide.points.length) return p;
1732
+ if (!this.start) {
1733
+ this.start = { x: p.x, y: p.y };
1734
+ return p;
1735
+ }
1736
+ const s = this.start;
1737
+ if (!this.dir) {
1738
+ const dx = p.x - s.x;
1739
+ const dy = p.y - s.y;
1740
+ const len = Math.hypot(dx, dy);
1741
+ if (len < SNAP_DECIDE_DISTANCE) return { ...p, x: s.x, y: s.y };
1742
+ let best = null;
1743
+ let bestDot = -1;
1744
+ for (const d2 of perspectiveDirections(this.guide, s)) {
1745
+ const dot = Math.abs((dx * d2.x + dy * d2.y) / len);
1746
+ if (dot > bestDot) {
1747
+ bestDot = dot;
1748
+ best = d2;
1749
+ }
1750
+ }
1751
+ this.dir = best;
1752
+ if (!best) return p;
1753
+ }
1754
+ const d = this.dir;
1755
+ const t = (p.x - s.x) * d.x + (p.y - s.y) * d.y;
1756
+ return { ...p, x: s.x + d.x * t, y: s.y + d.y * t };
1757
+ }
1758
+ };
1759
+ function perspectiveGuideLines(g, size, count = 12) {
1760
+ const out = [];
1761
+ const far = Math.hypot(size.width, size.height) * 4;
1762
+ for (const v of g.points) {
1763
+ const cx = size.width / 2 - v.x;
1764
+ const cy = size.height / 2 - v.y;
1765
+ const inside = v.x >= 0 && v.y >= 0 && v.x <= size.width && v.y <= size.height;
1766
+ const mid = Math.atan2(cy, cx);
1767
+ const spread = inside ? Math.PI : Math.min(Math.PI, 2 * Math.atan2(Math.hypot(size.width, size.height) / 2, Math.hypot(cx, cy)));
1768
+ for (let i = 0; i < count; i++) {
1769
+ const a = inside ? 2 * Math.PI * i / count : mid - spread / 2 + spread * (i + 0.5) / count;
1770
+ out.push([{ ...v }, { x: v.x + Math.cos(a) * far, y: v.y + Math.sin(a) * far }]);
1771
+ }
1772
+ }
1773
+ return out;
1774
+ }
1775
+
1776
+ // src/paint-editor/color.ts
1777
+ var HEX2 = /^#?([0-9a-f]{6})$/i;
1778
+ function parseHexColor(value, what = "color") {
1779
+ const m = typeof value === "string" ? HEX2.exec(value.trim()) : null;
1780
+ if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
1781
+ const n = parseInt(m[1], 16);
1782
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1783
+ }
1784
+ function rgbToHexColor(r, g, b) {
1785
+ return "#" + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("");
1786
+ }
1787
+ function rgbToHsv(r, g, b) {
1788
+ const R = r / 255;
1789
+ const G = g / 255;
1790
+ const B = b / 255;
1791
+ const max = Math.max(R, G, B);
1792
+ const min = Math.min(R, G, B);
1793
+ const d = max - min;
1794
+ let h = 0;
1795
+ if (d > 0) {
1796
+ if (max === R) h = 60 * ((G - B) / d % 6);
1797
+ else if (max === G) h = 60 * ((B - R) / d + 2);
1798
+ else h = 60 * ((R - G) / d + 4);
1799
+ }
1800
+ if (h < 0) h += 360;
1801
+ return { h, s: max === 0 ? 0 : d / max, v: max };
1802
+ }
1803
+ function hsvToRgb(h, s, v) {
1804
+ const hh = (h % 360 + 360) % 360 / 60;
1805
+ const c = v * s;
1806
+ const x = c * (1 - Math.abs(hh % 2 - 1));
1807
+ const m = v - c;
1808
+ const [r, g, b] = hh < 1 ? [c, x, 0] : hh < 2 ? [x, c, 0] : hh < 3 ? [0, c, x] : hh < 4 ? [0, x, c] : hh < 5 ? [x, 0, c] : [c, 0, x];
1809
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
1810
+ }
1811
+ function rgbToHsl(r, g, b) {
1812
+ const { h } = rgbToHsv(r, g, b);
1813
+ const max = Math.max(r, g, b) / 255;
1814
+ const min = Math.min(r, g, b) / 255;
1815
+ const l = (max + min) / 2;
1816
+ const d = max - min;
1817
+ return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };
1818
+ }
1819
+ function hslToRgb(h, s, l) {
1820
+ const c = (1 - Math.abs(2 * l - 1)) * s;
1821
+ const v = l + c / 2;
1822
+ return hsvToRgb(h, v === 0 ? 0 : c / v, v);
1823
+ }
1824
+ var RYB_STOPS = [
1825
+ [0, 0],
1826
+ [60, 30],
1827
+ [120, 60],
1828
+ [180, 120],
1829
+ [240, 240],
1830
+ [300, 280],
1831
+ [360, 360]
1832
+ ];
1833
+ function piecewise(v, from) {
1834
+ const x = (v % 360 + 360) % 360;
1835
+ const to = from === 0 ? 1 : 0;
1836
+ for (let i = 1; i < RYB_STOPS.length; i++) {
1837
+ const a = RYB_STOPS[i - 1];
1838
+ const b = RYB_STOPS[i];
1839
+ if (x <= b[from]) return a[to] + (x - a[from]) / (b[from] - a[from]) * (b[to] - a[to]);
1840
+ }
1841
+ return x;
1842
+ }
1843
+ function wheelToHue(angle, wheel) {
1844
+ return wheel === "rgb" ? (angle % 360 + 360) % 360 : piecewise(angle, 0);
1845
+ }
1846
+ function hueToWheel(hue, wheel) {
1847
+ return wheel === "rgb" ? (hue % 360 + 360) % 360 : piecewise(hue, 1);
1848
+ }
1849
+ var HARMONY_KINDS = ["complementary", "split-complementary", "analogous", "triadic", "tetradic", "square", "monochromatic"];
1850
+ function colorHarmony(base, kind, options = {}) {
1851
+ const wheel = options.wheel ?? "ryb";
1852
+ const [r, g, b] = parseHexColor(base, "base");
1853
+ const hsv = rgbToHsv(r, g, b);
1854
+ const start = hueToWheel(hsv.h, wheel);
1855
+ const at = (angle) => {
1856
+ const a = (angle % 360 + 360) % 360;
1857
+ const [rr, gg, bb] = hsvToRgb(wheelToHue(a, wheel), hsv.s, hsv.v);
1858
+ return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(a * 10) / 10 };
1859
+ };
1860
+ const spread = options.spread ?? 30;
1861
+ const offsets = {
1862
+ complementary: [0, 180],
1863
+ "split-complementary": [0, 180 - spread, 180 + spread],
1864
+ analogous: [0, -spread, spread],
1865
+ triadic: [0, 120, 240],
1866
+ tetradic: [0, 60, 180, 240],
1867
+ square: [0, 90, 180, 270]
1868
+ };
1869
+ if (kind === "monochromatic") {
1870
+ const hsl = rgbToHsl(r, g, b);
1871
+ const steps = [0, -0.3, -0.15, 0.15, 0.3];
1872
+ return steps.map((d) => {
1873
+ const [rr, gg, bb] = d === 0 ? [r, g, b] : hslToRgb(hsl.h, hsl.s, Math.min(0.97, Math.max(0.03, hsl.l + d)));
1874
+ return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(start * 10) / 10 };
1875
+ });
1876
+ }
1877
+ return offsets[kind].map((o, i) => i === 0 ? { color: rgbToHexColor(r, g, b), angle: Math.round(start * 10) / 10 } : at(start + o));
1878
+ }
1879
+ var DEFAULT_SWATCHES = [
1880
+ { color: "#f7f5ee", name: "Warm white" },
1881
+ { color: "#f3d23a", name: "Lemon yellow" },
1882
+ { color: "#c8912f", name: "Yellow ochre" },
1883
+ { color: "#d23a2a", name: "Warm red" },
1884
+ { color: "#9e1f3d", name: "Crimson" },
1885
+ { color: "#8a4a2b", name: "Burnt earth" },
1886
+ { color: "#2f3f9e", name: "Deep blue" },
1887
+ { color: "#1d6f8f", name: "Cool blue" },
1888
+ { color: "#4f6e2c", name: "Leaf green" },
1889
+ { color: "#25221f", name: "Warm black" }
1890
+ ];
1891
+
1892
+ // src/paint-editor/mixing-pad.ts
1893
+ var MIXING_PAD_WIDTH = 360;
1894
+ var MIXING_PAD_HEIGHT = 220;
1895
+ var PAD_PAINT = normalizePaintBrush({
1896
+ id: "pad-paint",
1897
+ name: "Pad paint",
1898
+ category: "mixer",
1899
+ engine: "mixer",
1900
+ tip: { size: 28, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 0.7, pressureSize: false, pressureOpacity: false },
1901
+ mixer: { wet: 0.45, load: 1, mix: 0.4, loadEachStroke: true, cleanEachStroke: true, mixing: "pigment" }
1902
+ });
1903
+ var PAD_KNIFE = normalizePaintBrush({
1904
+ id: "pad-knife",
1905
+ name: "Palette knife",
1906
+ category: "smudge",
1907
+ engine: "mixer",
1908
+ loadsColor: false,
1909
+ tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow: 0.85, pressureSize: false, pressureOpacity: false },
1910
+ mixer: { wet: 1, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: "pigment" }
1911
+ });
1912
+ var MixingPad = class {
1913
+ width = MIXING_PAD_WIDTH;
1914
+ height = MIXING_PAD_HEIGHT;
1915
+ surface = emptySurface(4);
1916
+ /** Increments on every change (for redrawing). */
1917
+ version = 0;
1918
+ /** Paints (`paint`, with `color`) or smears (`knife`) through `points` in pad pixels. */
1919
+ stroke(points, options) {
1920
+ if (!points.length) throw new Error("points is empty; give at least one {x, y} in pad pixels.");
1921
+ for (const [i, p] of points.entries()) {
1922
+ if (!Number.isFinite(p.x) || !Number.isFinite(p.y) || p.x < 0 || p.y < 0 || p.x > this.width || p.y > this.height) {
1923
+ throw new Error(`points[${i}] (${p.x}, ${p.y}) is off the ${this.width}\xD7${this.height} mixing pad.`);
1924
+ }
1925
+ }
1926
+ let brush = options.tool === "knife" ? PAD_KNIFE : PAD_PAINT;
1927
+ if (options.size !== void 0) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);
1928
+ const [r, g, b] = hexToRgb8(options.color);
1929
+ let result = null;
1930
+ const session = new PaintStrokeSession(
1931
+ { update: () => {
1932
+ }, commit: (_s, s) => result = s },
1933
+ {
1934
+ layerId: "mixing-pad",
1935
+ surface: this.surface,
1936
+ offset: { x: 0, y: 0 },
1937
+ lockTransparency: false,
1938
+ brush,
1939
+ color: [r, g, b, 255],
1940
+ background: [255, 255, 255, 255],
1941
+ grain: null,
1942
+ paperDepth: 0,
1943
+ seed: 1,
1944
+ colorHex: options.color,
1945
+ affineCopies: [],
1946
+ copies: [IDENTITY_COPY],
1947
+ mixerLoad: mixerLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
1948
+ }
1949
+ );
1950
+ session.push(points);
1951
+ const res = session.end();
1952
+ if (result) {
1953
+ this.surface = result;
1954
+ this.version++;
1955
+ }
1956
+ return { bounds: res.bounds };
1957
+ }
1958
+ /** The average colour within `radius` pixels of (x, y), or `null` where the pad is bare. */
1959
+ pick(x, y, radius = 3) {
1960
+ if (!(x >= 0 && y >= 0 && x < this.width && y < this.height)) throw new Error(`(${x}, ${y}) is off the ${this.width}\xD7${this.height} mixing pad.`);
1961
+ const r = Math.max(0, Math.round(radius));
1962
+ const x0 = Math.max(0, Math.floor(x) - r);
1963
+ const y0 = Math.max(0, Math.floor(y) - r);
1964
+ const x1 = Math.min(this.width, Math.floor(x) + r + 1);
1965
+ const y1 = Math.min(this.height, Math.floor(y) + r + 1);
1966
+ const px = readRegion(this.surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
1967
+ let R = 0;
1968
+ let G = 0;
1969
+ let B = 0;
1970
+ let A = 0;
1971
+ for (let i = 0; i < px.length; i += 4) {
1972
+ const a = px[i + 3];
1973
+ R += px[i] * a;
1974
+ G += px[i + 1] * a;
1975
+ B += px[i + 2] * a;
1976
+ A += a;
1977
+ }
1978
+ if (A === 0) return null;
1979
+ const hex = (v) => Math.round(v / A).toString(16).padStart(2, "0");
1980
+ return `#${hex(R)}${hex(G)}${hex(B)}`;
1981
+ }
1982
+ clear() {
1983
+ this.surface = emptySurface(4);
1984
+ this.version++;
1985
+ }
1986
+ /** The pad over white, `width × height` RGBA. */
1987
+ pixels() {
1988
+ const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
1989
+ for (let i = 0; i < px.length; i += 4) {
1990
+ const a = px[i + 3] / 255;
1991
+ px[i] = Math.round(px[i] * a + 255 * (1 - a));
1992
+ px[i + 1] = Math.round(px[i + 1] * a + 255 * (1 - a));
1993
+ px[i + 2] = Math.round(px[i + 2] * a + 255 * (1 - a));
1994
+ px[i + 3] = 255;
1995
+ }
1996
+ return px;
1997
+ }
1998
+ };
1999
+
1565
2000
  // src/paint-editor/controller.ts
1566
2001
  var PaintEditorError = class extends Error {
1567
2002
  constructor(message) {
@@ -1569,9 +2004,9 @@ var PaintEditorError = class extends Error {
1569
2004
  this.name = "PaintEditorError";
1570
2005
  }
1571
2006
  };
1572
- var HEX2 = /^#[0-9a-f]{6}$/i;
2007
+ var HEX3 = /^#[0-9a-f]{6}$/i;
1573
2008
  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".`);
2009
+ if (typeof value !== "string" || !HEX3.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
1575
2010
  return value.trim().toLowerCase();
1576
2011
  }
1577
2012
  function rgbaOf(hex) {
@@ -1612,6 +2047,13 @@ var PaintEditorController = class {
1612
2047
  dirty = "all";
1613
2048
  seedCounter = 1;
1614
2049
  strokeCounter = 1;
2050
+ guides;
2051
+ swatches = DEFAULT_SWATCHES.map((s) => ({ ...s }));
2052
+ pad = new MixingPad();
2053
+ tracingVersion = 1;
2054
+ tracingCache = null;
2055
+ imagePixels = /* @__PURE__ */ new Map();
2056
+ nextReferenceId = 1;
1615
2057
  viewCompositor = createPaintCompositor();
1616
2058
  exportCompositor = createPaintCompositor();
1617
2059
  constructor(options = {}) {
@@ -1623,6 +2065,7 @@ var PaintEditorController = class {
1623
2065
  this.color = options.color ? parseColor(options.color) : "#1f2937";
1624
2066
  this.hand = options.hand === "left" ? "left" : "right";
1625
2067
  this.encodeImage = options.encodeImage;
2068
+ this.guides = defaultGuides(this.doc.canvas);
1626
2069
  }
1627
2070
  // --- State and events ----------------------------------------------------------
1628
2071
  subscribe(listener) {
@@ -1654,6 +2097,9 @@ var PaintEditorController = class {
1654
2097
  canRedo: this.redoStack.length > 0 && !this.session,
1655
2098
  undoLabel: last?.label ?? null,
1656
2099
  redoLabel: next?.label ?? null,
2100
+ guides: this.guides,
2101
+ swatches: this.swatches,
2102
+ mixingPadVersion: this.pad.version,
1657
2103
  stroking: Boolean(this.session),
1658
2104
  version: this.version
1659
2105
  };
@@ -1723,6 +2169,8 @@ var PaintEditorController = class {
1723
2169
  paper: options.paper === void 0 ? this.doc.paper : this.resolvePaper(options.paper)
1724
2170
  });
1725
2171
  this.extensions = {};
2172
+ this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
2173
+ this.tracingVersion++;
1726
2174
  this.commit("New canvas", next);
1727
2175
  return next;
1728
2176
  }
@@ -2145,7 +2593,7 @@ var PaintEditorController = class {
2145
2593
  }
2146
2594
  /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
2147
2595
  strokeCopies() {
2148
- return [];
2596
+ return symmetryCopies(this.guides.symmetry);
2149
2597
  }
2150
2598
  sessionFor(doc, init, host) {
2151
2599
  const layer = (() => {
@@ -2160,7 +2608,11 @@ var PaintEditorController = class {
2160
2608
  return l;
2161
2609
  })();
2162
2610
  const brush = this.strokeBrush(init);
2163
- const color = init.color === void 0 ? this.color : parseColor(init.color);
2611
+ let color = init.color === void 0 ? this.color : parseColor(init.color);
2612
+ const tracing = this.guides.tracing;
2613
+ if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
2614
+ const perspective = this.guides.perspective;
2615
+ const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;
2164
2616
  const rgba = rgbaOf(color);
2165
2617
  const seed = init.seed ?? mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647;
2166
2618
  const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
@@ -2178,6 +2630,7 @@ var PaintEditorController = class {
2178
2630
  colorHex: color,
2179
2631
  affineCopies: copies,
2180
2632
  copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
2633
+ inputFilter: snapper ? (p) => snapper.snap(p) : void 0,
2181
2634
  mixerLoad: brush.engine === "mixer" ? mixerLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]) : EMPTY_MIXER_LOAD
2182
2635
  });
2183
2636
  return session;
@@ -2222,7 +2675,7 @@ var PaintEditorController = class {
2222
2675
  this.commit(`${brush.name} stroke`, { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke] });
2223
2676
  }
2224
2677
  };
2225
- const session = this.sessionFor(this.doc, init, host);
2678
+ const session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);
2226
2679
  this.session = session;
2227
2680
  this.emit([]);
2228
2681
  return session;
@@ -2254,7 +2707,7 @@ var PaintEditorController = class {
2254
2707
  paintStroke(init) {
2255
2708
  this.requireIdle("painting");
2256
2709
  const points = this.checkPoints(init.points);
2257
- const { doc, result, brush } = this.runStroke(this.doc, init, points);
2710
+ const { doc, result, brush } = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);
2258
2711
  if (result.mixerLoad && brush.loadsColor) this.mixerLoad = result.mixerLoad;
2259
2712
  if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);
2260
2713
  else this.emit([]);
@@ -2339,7 +2792,20 @@ var PaintEditorController = class {
2339
2792
  }
2340
2793
  /** View-only layers between the paper and the paint (the painting guides add the tracing image). */
2341
2794
  underlayLayers() {
2342
- return [];
2795
+ const t = this.guides.tracing;
2796
+ if (!t || !t.visible) return [];
2797
+ return [this.tracingNode(t)];
2798
+ }
2799
+ /** The tracing image resampled onto the canvas (cached until it or the canvas changes). */
2800
+ tracingNode(t) {
2801
+ const { width, height } = this.doc.canvas;
2802
+ const key = this.tracingVersion * 100003 + width * 7 + height;
2803
+ if (this.tracingCache?.key === key && this.tracingCache.node.opacity === t.opacity) return this.tracingCache.node;
2804
+ const src = this.imagePixels.get(t.src) ?? this.decodeImage(t.src);
2805
+ const image = resampleInto(src, t.rect, width, height);
2806
+ const node = { type: "image", id: TRACING_NODE_ID, name: "Tracing", visible: true, opacity: t.opacity, blend: "normal", image, version: key + Math.round(t.opacity * 1e3) / 1e4 };
2807
+ this.tracingCache = { key, node };
2808
+ return node;
2343
2809
  }
2344
2810
  renderLayers(options, view = false) {
2345
2811
  let layers = this.currentLayers();
@@ -2380,14 +2846,18 @@ var PaintEditorController = class {
2380
2846
  // --- Save, load and export -------------------------------------------------------
2381
2847
  /** The document as JSON with inline PNG assets. */
2382
2848
  toJSON(options = {}) {
2383
- return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions });
2849
+ return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
2384
2850
  }
2385
2851
  /** The document as JSON with each PNG stored in `store`. */
2386
2852
  toJSONExternal(store, options = {}) {
2387
- return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions });
2853
+ return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
2388
2854
  }
2389
2855
  adopt(loaded) {
2390
2856
  this.extensions = loaded.extensions;
2857
+ this.readGuides(loaded.guides, loaded.state, loaded.problems);
2858
+ if (Array.isArray(loaded.swatches)) {
2859
+ this.swatches = loaded.swatches.filter((x) => Boolean(x) && typeof x.color === "string" && /^#[0-9a-f]{6}$/i.test(x.color)).map((x) => typeof x.name === "string" ? { color: x.color.toLowerCase(), name: x.name } : { color: x.color.toLowerCase() });
2860
+ }
2391
2861
  for (const s of loaded.state.strokes) {
2392
2862
  const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
2393
2863
  if (n >= this.strokeCounter) this.strokeCounter = n + 1;
@@ -2476,20 +2946,630 @@ var PaintEditorController = class {
2476
2946
  const img = this.renderPreview(options);
2477
2947
  return { data: bytesToBase64(encodePng(img.width, img.height, img.data)), width: img.width, height: img.height };
2478
2948
  }
2949
+ // --- Guides --------------------------------------------------------------------
2950
+ setGuides(next) {
2951
+ this.guides = next;
2952
+ this.emit("all");
2953
+ return next;
2954
+ }
2955
+ /** Mirror and kaleidoscope painting: every new stroke paints its copies too (one stroke, one undo step). */
2956
+ setSymmetry(update) {
2957
+ const symmetry = normalizeSymmetry(update, this.guides.symmetry);
2958
+ this.setGuides({ ...this.guides, symmetry });
2959
+ return symmetry;
2960
+ }
2961
+ /** Perspective guides with one to three vanishing points that pointer strokes snap to. */
2962
+ setPerspective(update) {
2963
+ let perspective;
2964
+ try {
2965
+ perspective = normalizePerspective(update, this.guides.perspective);
2966
+ } catch (e) {
2967
+ throw new PaintEditorError(e.message);
2968
+ }
2969
+ this.setGuides({ ...this.guides, perspective });
2970
+ return perspective;
2971
+ }
2972
+ /** An image from a PNG (base64 or data URL) or from RGBA pixels, stored as a PNG data URL. */
2973
+ importImage(input, what) {
2974
+ let pixels;
2975
+ if (input.pixels) {
2976
+ const { width, height, data } = input.pixels;
2977
+ if (!(width > 0 && height > 0 && Number.isInteger(width) && Number.isInteger(height)) || data.length !== width * height * 4) {
2978
+ throw new PaintEditorError(`${what}: pixels must be width \xD7 height \xD7 4 bytes of RGBA.`);
2979
+ }
2980
+ pixels = { width, height, data: new Uint8ClampedArray(data) };
2981
+ } else if (typeof input.png === "string" && input.png.trim()) {
2982
+ pixels = this.decodeImage(input.png.trim().startsWith("data:") ? input.png.trim() : `data:image/png;base64,${input.png.trim()}`, what);
2983
+ } else throw new PaintEditorError(`${what}: pass png (a PNG as base64 or a data URL) or pixels.`);
2984
+ if (pixels.width > MAX_IMAGE_SIDE || pixels.height > MAX_IMAGE_SIDE) throw new PaintEditorError(`${what} is ${pixels.width}\xD7${pixels.height}; the limit is ${MAX_IMAGE_SIDE} px per side.`);
2985
+ const src = pngDataUrl(pixels.width, pixels.height, pixels.data);
2986
+ this.imagePixels.set(src, pixels);
2987
+ return { src, pixels };
2988
+ }
2989
+ decodeImage(src, what = "Image") {
2990
+ const cached = this.imagePixels.get(src);
2991
+ if (cached) return cached;
2992
+ const parsed = parseDataUrl(src);
2993
+ if (!parsed) throw new PaintEditorError(`${what} is not a data URL or base64 PNG.`);
2994
+ try {
2995
+ const png = decodePng(parsed.bytes);
2996
+ const px = { width: png.width, height: png.height, data: png.data };
2997
+ this.imagePixels.set(src, px);
2998
+ return px;
2999
+ } catch (e) {
3000
+ throw new PaintEditorError(`${what} could not be decoded as a PNG (${e.message}); decode other formats in the browser and pass pixels.`);
3001
+ }
3002
+ }
3003
+ /**
3004
+ * Puts an image under the paint to trace over: view only, never in exports. Placed to fit the
3005
+ * canvas unless `rect` is given; `sampleColors` makes strokes take their colour from it.
3006
+ */
3007
+ setTracingImage(options) {
3008
+ const { src, pixels } = this.importImage(options, "The tracing image");
3009
+ const { width, height } = this.doc.canvas;
3010
+ const k = Math.min(width / pixels.width, height / pixels.height);
3011
+ const fit = { x: (width - pixels.width * k) / 2, y: (height - pixels.height * k) / 2, width: pixels.width * k, height: pixels.height * k };
3012
+ const tracing = { src, width: pixels.width, height: pixels.height, rect: fit, opacity: 0.35, visible: true, sampleColors: false };
3013
+ return this.updateTracing({ ...options, rect: options.rect ?? fit }, tracing);
3014
+ }
3015
+ /** Changes the tracing image's opacity, visibility, placement or colour sampling. */
3016
+ updateTracing(update, base = this.guides.tracing) {
3017
+ if (!base) throw new PaintEditorError("There is no tracing image; set one with set_tracing_image first.");
3018
+ if (update.opacity !== void 0 && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);
3019
+ if (update.rect && !(update.rect.width > 0 && update.rect.height > 0)) throw new PaintEditorError("rect needs a width and height above 0.");
3020
+ const tracing = {
3021
+ ...base,
3022
+ ...update.opacity !== void 0 ? { opacity: update.opacity } : {},
3023
+ ...update.visible !== void 0 ? { visible: update.visible } : {},
3024
+ ...update.sampleColors !== void 0 ? { sampleColors: update.sampleColors } : {},
3025
+ ...update.rect ? { rect: { ...update.rect } } : {}
3026
+ };
3027
+ if (tracing.src !== base.src || update.rect) this.tracingVersion++;
3028
+ if (base !== this.guides.tracing) this.tracingVersion++;
3029
+ this.setGuides({ ...this.guides, tracing });
3030
+ return tracing;
3031
+ }
3032
+ clearTracingImage() {
3033
+ this.tracingVersion++;
3034
+ this.setGuides({ ...this.guides, tracing: null });
3035
+ }
3036
+ /** The tracing image's colour at a document pixel, or `null` outside it or where it is clear. */
3037
+ sampleTracingColor(x, y) {
3038
+ const t = this.guides.tracing;
3039
+ if (!t) return null;
3040
+ const src = this.decodeImage(t.src);
3041
+ const u = Math.floor((x - t.rect.x) / t.rect.width * src.width);
3042
+ const v = Math.floor((y - t.rect.y) / t.rect.height * src.height);
3043
+ if (u < 0 || v < 0 || u >= src.width || v >= src.height) return null;
3044
+ const i = (v * src.width + u) * 4;
3045
+ if (src.data[i + 3] === 0) return null;
3046
+ return rgbToHex(src.data[i], src.data[i + 1], src.data[i + 2]);
3047
+ }
3048
+ /** Adds a floating reference image to look at and pick colours from. */
3049
+ addReferenceImage(options) {
3050
+ const { src, pixels } = this.importImage(options, "The reference image");
3051
+ const ref = {
3052
+ id: `ref${this.nextReferenceId++}`,
3053
+ name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,
3054
+ src,
3055
+ width: pixels.width,
3056
+ height: pixels.height,
3057
+ x: options.x ?? 16 + this.guides.references.length * 24,
3058
+ y: options.y ?? 16 + this.guides.references.length * 24,
3059
+ displayWidth: options.displayWidth ?? Math.min(240, pixels.width),
3060
+ visible: true
3061
+ };
3062
+ this.setGuides({ ...this.guides, references: [...this.guides.references, ref] });
3063
+ return ref;
3064
+ }
3065
+ requireReference(id) {
3066
+ const r = this.guides.references.find((x) => x.id === id);
3067
+ if (!r) throw new PaintEditorError(`Unknown reference image "${id}". Call list_reference_images for the ids.`);
3068
+ return r;
3069
+ }
3070
+ /** Moves, resizes, renames, shows or hides a reference image. */
3071
+ updateReferenceImage(id, update) {
3072
+ const r = this.requireReference(id);
3073
+ if (update.displayWidth !== void 0 && !(update.displayWidth >= 16 && update.displayWidth <= 4096)) throw new PaintEditorError(`displayWidth ${update.displayWidth} is out of range; use 16..4096.`);
3074
+ const next = { ...r, ...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== void 0)) };
3075
+ this.setGuides({ ...this.guides, references: this.guides.references.map((x) => x.id === id ? next : x) });
3076
+ return next;
3077
+ }
3078
+ removeReferenceImage(id) {
3079
+ this.requireReference(id);
3080
+ this.setGuides({ ...this.guides, references: this.guides.references.filter((x) => x.id !== id) });
3081
+ }
3082
+ /** A reference image's pixels (decoded once). */
3083
+ referencePixels(id) {
3084
+ return this.decodeImage(this.requireReference(id).src);
3085
+ }
3086
+ /** The colour of a reference image at its pixel (x, y). */
3087
+ pickReferenceColor(id, x, y) {
3088
+ const r = this.requireReference(id);
3089
+ const px = this.decodeImage(r.src);
3090
+ const u = Math.floor(x);
3091
+ const v = Math.floor(y);
3092
+ if (!(u >= 0 && v >= 0 && u < px.width && v < px.height)) throw new PaintEditorError(`(${x}, ${y}) is outside reference "${r.name}" (${px.width}\xD7${px.height}); use its own pixel coordinates.`);
3093
+ const i = (v * px.width + u) * 4;
3094
+ return rgbToHex(px.data[i], px.data[i + 1], px.data[i + 2]);
3095
+ }
3096
+ readGuides(raw, state, problems) {
3097
+ const base = defaultGuides(state.canvas);
3098
+ this.guides = base;
3099
+ this.tracingVersion++;
3100
+ if (!raw || typeof raw !== "object") return;
3101
+ const r = raw;
3102
+ const guides = { ...base };
3103
+ guides.symmetry = normalizeSymmetry(r.symmetry ?? {}, base.symmetry);
3104
+ try {
3105
+ guides.perspective = normalizePerspective(r.perspective ?? {}, base.perspective);
3106
+ } catch (e) {
3107
+ problems.push(`Perspective guide: ${e.message}`);
3108
+ }
3109
+ const t = r.tracing;
3110
+ if (t && typeof t.src === "string") {
3111
+ try {
3112
+ const px = this.decodeImage(t.src, "The tracing image");
3113
+ guides.tracing = {
3114
+ src: t.src,
3115
+ width: px.width,
3116
+ height: px.height,
3117
+ rect: t.rect && t.rect.width > 0 && t.rect.height > 0 ? { ...t.rect } : { x: 0, y: 0, width: state.canvas.width, height: state.canvas.height },
3118
+ opacity: typeof t.opacity === "number" ? Math.min(1, Math.max(0, t.opacity)) : 0.35,
3119
+ visible: t.visible !== false,
3120
+ sampleColors: t.sampleColors === true
3121
+ };
3122
+ } catch (e) {
3123
+ problems.push(e.message);
3124
+ }
3125
+ }
3126
+ if (Array.isArray(r.references)) {
3127
+ for (const ref of r.references) {
3128
+ if (!ref || typeof ref.src !== "string" || typeof ref.id !== "string") continue;
3129
+ try {
3130
+ const px = this.decodeImage(ref.src, `Reference "${ref.name}"`);
3131
+ guides.references.push({ id: ref.id, name: String(ref.name ?? ref.id), src: ref.src, width: px.width, height: px.height, x: Number(ref.x) || 0, y: Number(ref.y) || 0, displayWidth: Number(ref.displayWidth) || 240, visible: ref.visible !== false });
3132
+ const n = Number(/^ref(\d+)$/.exec(ref.id)?.[1] ?? 0);
3133
+ if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;
3134
+ } catch (e) {
3135
+ problems.push(e.message);
3136
+ }
3137
+ }
3138
+ }
3139
+ this.guides = guides;
3140
+ }
3141
+ // --- Swatches and harmonies ------------------------------------------------------
3142
+ /** Colour harmonies around a colour (default the paint colour) on the painter's RYB wheel or the RGB wheel. */
3143
+ harmony(kind, options = {}) {
3144
+ try {
3145
+ return colorHarmony(options.color === void 0 ? this.color : parseColor(options.color), kind, options);
3146
+ } catch (e) {
3147
+ throw new PaintEditorError(e.message);
3148
+ }
3149
+ }
3150
+ addSwatch(color, name) {
3151
+ const c = parseColor(color);
3152
+ this.swatches = [...this.swatches, name?.trim() ? { color: c, name: name.trim() } : { color: c }];
3153
+ this.emit([]);
3154
+ return this.swatches;
3155
+ }
3156
+ removeSwatch(index) {
3157
+ if (!Number.isInteger(index) || index < 0 || index >= this.swatches.length) throw new PaintEditorError(`index ${index} is out of range; there are ${this.swatches.length} swatches (0-based).`);
3158
+ this.swatches = this.swatches.filter((_, i) => i !== index);
3159
+ this.emit([]);
3160
+ return this.swatches;
3161
+ }
3162
+ setSwatches(swatches) {
3163
+ this.swatches = swatches.map((x) => x.name ? { color: parseColor(x.color), name: x.name } : { color: parseColor(x.color) });
3164
+ this.emit([]);
3165
+ return this.swatches;
3166
+ }
3167
+ // --- Mixing pad ------------------------------------------------------------------
3168
+ get mixingPad() {
3169
+ return this.pad;
3170
+ }
3171
+ /** Paints (`paint`) or smears with the palette knife (`knife`) on the mixing pad, in pad pixels. */
3172
+ mixingPadStroke(points, options = {}) {
3173
+ const color = options.color === void 0 ? this.color : parseColor(options.color);
3174
+ if (options.size !== void 0 && !(options.size >= 1 && options.size <= 200)) throw new PaintEditorError(`size ${options.size} is out of range; use 1..200 px.`);
3175
+ try {
3176
+ const r = this.pad.stroke(points, { tool: options.tool ?? "paint", color, size: options.size });
3177
+ this.emit([]);
3178
+ return r;
3179
+ } catch (e) {
3180
+ throw new PaintEditorError(e.message);
3181
+ }
3182
+ }
3183
+ /** Picks the mixed colour on the pad (averaged over `radius` px); with `setColor` it becomes the paint colour. */
3184
+ mixingPadPick(x, y, options = {}) {
3185
+ let c;
3186
+ try {
3187
+ c = this.pad.pick(x, y, options.radius);
3188
+ } catch (e) {
3189
+ throw new PaintEditorError(e.message);
3190
+ }
3191
+ if (c && options.setColor) this.setColor(c);
3192
+ return c;
3193
+ }
3194
+ clearMixingPad() {
3195
+ this.pad.clear();
3196
+ this.emit([]);
3197
+ }
2479
3198
  /** A layer's tile surface (live while stroking), for thumbnails. */
2480
3199
  layerSurface(id) {
2481
3200
  const l = findLayer(this.currentLayers(), id)?.layer;
2482
3201
  return isPixelLayer(l) ? l.surface : null;
2483
3202
  }
2484
3203
  };
3204
+ var MAX_IMAGE_SIDE = 8192;
3205
+ function resampleInto(src, rect, width, height) {
3206
+ const out = new Uint8ClampedArray(width * height * 4);
3207
+ const x0 = Math.max(0, Math.floor(rect.x));
3208
+ const y0 = Math.max(0, Math.floor(rect.y));
3209
+ const x1 = Math.min(width, Math.ceil(rect.x + rect.width));
3210
+ const y1 = Math.min(height, Math.ceil(rect.y + rect.height));
3211
+ const sx = src.width / rect.width;
3212
+ const sy = src.height / rect.height;
3213
+ const d = src.data;
3214
+ for (let y = y0; y < y1; y++) {
3215
+ const v = Math.min(src.height - 1, Math.max(0, (y + 0.5 - rect.y) * sy - 0.5));
3216
+ const j0 = Math.floor(v);
3217
+ const j1 = Math.min(src.height - 1, j0 + 1);
3218
+ const fy = v - j0;
3219
+ for (let x = x0; x < x1; x++) {
3220
+ const u = (x + 0.5 - rect.x) * sx - 0.5;
3221
+ if (u < -0.5 || u > src.width - 0.5 || (y + 0.5 - rect.y) * sy < 0 || (y + 0.5 - rect.y) * sy > src.height) continue;
3222
+ const uc = Math.min(src.width - 1, Math.max(0, u));
3223
+ const i0 = Math.floor(uc);
3224
+ const i1 = Math.min(src.width - 1, i0 + 1);
3225
+ const fx = uc - i0;
3226
+ const o = (y * width + x) * 4;
3227
+ for (let c = 0; c < 4; c++) {
3228
+ const a = d[(j0 * src.width + i0) * 4 + c] + (d[(j0 * src.width + i1) * 4 + c] - d[(j0 * src.width + i0) * 4 + c]) * fx;
3229
+ const b = d[(j1 * src.width + i0) * 4 + c] + (d[(j1 * src.width + i1) * 4 + c] - d[(j1 * src.width + i0) * 4 + c]) * fx;
3230
+ out[o + c] = a + (b - a) * fy;
3231
+ }
3232
+ }
3233
+ }
3234
+ return { width, height, data: out };
3235
+ }
2485
3236
 
2486
- // src/paint-editor/tools.ts
3237
+ // src/paint-editor/guide-tools.ts
2487
3238
  var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
2488
3239
  var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
2489
3240
  var SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
2490
3241
  var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
2491
3242
  var EMPTY = { type: "object", properties: {}, additionalProperties: false };
2492
- var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
3243
+ var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Colour #rrggbb." };
3244
+ var POINT = {
3245
+ type: "object",
3246
+ properties: { x: { type: "number", description: "x in document pixels." }, y: { type: "number", description: "y in document pixels." } },
3247
+ required: ["x", "y"],
3248
+ additionalProperties: false
3249
+ };
3250
+ var RECT = {
3251
+ type: "object",
3252
+ description: "Placement in document pixels.",
3253
+ properties: {
3254
+ x: { type: "number", description: "Left edge." },
3255
+ y: { type: "number", description: "Top edge." },
3256
+ width: { type: "number", exclusiveMinimum: 0, description: "Width." },
3257
+ height: { type: "number", exclusiveMinimum: 0, description: "Height." }
3258
+ },
3259
+ required: ["x", "y", "width", "height"],
3260
+ additionalProperties: false
3261
+ };
3262
+ var PNG = { type: "string", minLength: 1, description: "The image as a PNG, base64 or a data:image/png URL." };
3263
+ var tracingInfo = (t) => t ? { width: t.width, height: t.height, rect: t.rect, opacity: t.opacity, visible: t.visible, sampleColors: t.sampleColors } : null;
3264
+ var refInfo = ({ src: _s, ...r }) => r;
3265
+ function createPaintGuideTools(c) {
3266
+ const guides = () => c.getState().guides;
3267
+ return [
3268
+ {
3269
+ name: "get_guides",
3270
+ title: "Get the painting guides",
3271
+ description: "Reads the painting guides: symmetry (mode, centre, rotation, segments, mirror, and how many copies each stroke paints), the perspective guide (vanishing points, snap), the tracing image (size, placement, opacity, visibility, colour sampling) and the reference images.",
3272
+ inputSchema: EMPTY,
3273
+ annotations: { title: "Get the painting guides", ...READ },
3274
+ handler: () => {
3275
+ const g = guides();
3276
+ return { symmetry: { ...g.symmetry, copies: symmetryCopies(g.symmetry).length }, perspective: g.perspective, tracing: tracingInfo(g.tracing), references: g.references.map(refInfo) };
3277
+ }
3278
+ },
3279
+ {
3280
+ name: "set_symmetry",
3281
+ title: "Set mirror or kaleidoscope painting",
3282
+ description: "Turns symmetry on or off. Every stroke after it also paints its copies, as one stroke and one undo step: vertical, horizontal or diagonal mirror (one copy), dual (four-way), or kaleidoscope with n segments (rotated copies, and with mirror the reflections between them). Editor state, saved with the document. Returns the guide and the number of copies.",
3283
+ inputSchema: {
3284
+ type: "object",
3285
+ properties: {
3286
+ mode: { type: "string", enum: [...SYMMETRY_MODES], description: "off, vertical (mirror left / right), horizontal (top / bottom), dual, diagonal or kaleidoscope." },
3287
+ center: { ...POINT, description: "Centre of the axes in document pixels (default the canvas centre)." },
3288
+ rotation: { type: "number", minimum: -360, maximum: 360, description: "Turn the axes, degrees clockwise." },
3289
+ segments: { type: "integer", minimum: 2, maximum: 32, description: "Kaleidoscope segments (default 6)." },
3290
+ mirror: { type: "boolean", description: "Kaleidoscope: also reflect alternate segments (default true)." }
3291
+ },
3292
+ additionalProperties: false
3293
+ },
3294
+ annotations: { title: "Set mirror or kaleidoscope painting", ...SET },
3295
+ handler: (a) => {
3296
+ const s = c.setSymmetry(a);
3297
+ return { ...s, copies: symmetryCopies(s).length };
3298
+ }
3299
+ },
3300
+ {
3301
+ name: "set_perspective_guide",
3302
+ title: "Set the perspective guide",
3303
+ description: "Sets one to three vanishing points (document pixels, may be off the canvas). With snap on, pen strokes snap to the line through their start towards a vanishing point (one-point also keeps horizontals and verticals, two-point verticals); for paint_stroke pass snapToPerspective. Editor state, saved with the document. Returns the guide.",
3304
+ inputSchema: {
3305
+ type: "object",
3306
+ properties: {
3307
+ enabled: { type: "boolean", description: "Show and use the guide." },
3308
+ points: { type: "array", minItems: 1, maxItems: 3, items: { ...POINT, description: "A vanishing point." }, description: "The vanishing points." },
3309
+ snap: { type: "boolean", description: "Snap strokes to the guide lines (default true)." }
3310
+ },
3311
+ additionalProperties: false
3312
+ },
3313
+ annotations: { title: "Set the perspective guide", ...SET },
3314
+ handler: (a) => c.setPerspective(a)
3315
+ },
3316
+ {
3317
+ name: "set_tracing_image",
3318
+ title: "Set a tracing image",
3319
+ description: "Puts an image under the paint to trace over (view only: never exported), fitted to the canvas unless rect is given, at opacity (default 0.35). With sampleColors, strokes without a colour take the image colour where they start, so painting over it copies its colours. Replaces any tracing image. Returns its size and placement.",
3320
+ inputSchema: {
3321
+ type: "object",
3322
+ properties: {
3323
+ png: PNG,
3324
+ rect: RECT,
3325
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 0.35)." },
3326
+ sampleColors: { type: "boolean", description: "Strokes take their colour from the image (default false)." }
3327
+ },
3328
+ required: ["png"],
3329
+ additionalProperties: false
3330
+ },
3331
+ annotations: { title: "Set a tracing image", ...WRITE },
3332
+ handler: (a) => tracingInfo(c.setTracingImage(a))
3333
+ },
3334
+ {
3335
+ name: "update_tracing_image",
3336
+ title: "Change the tracing image",
3337
+ description: "Changes the tracing image opacity, visibility, placement or colour sampling. Returns it.",
3338
+ inputSchema: {
3339
+ type: "object",
3340
+ properties: {
3341
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1." },
3342
+ visible: { type: "boolean", description: "Show or hide it." },
3343
+ sampleColors: { type: "boolean", description: "Strokes take their colour from it." },
3344
+ rect: RECT
3345
+ },
3346
+ additionalProperties: false
3347
+ },
3348
+ annotations: { title: "Change the tracing image", ...SET },
3349
+ handler: (a) => tracingInfo(c.updateTracing(a))
3350
+ },
3351
+ {
3352
+ name: "clear_tracing_image",
3353
+ title: "Remove the tracing image",
3354
+ description: "Removes the tracing image. Returns null.",
3355
+ inputSchema: EMPTY,
3356
+ annotations: { title: "Remove the tracing image", ...DESTRUCTIVE },
3357
+ handler: () => {
3358
+ c.clearTracingImage();
3359
+ return { tracing: null };
3360
+ }
3361
+ },
3362
+ {
3363
+ name: "add_reference_image",
3364
+ title: "Add a reference image",
3365
+ description: "Adds a floating reference image (shown beside the canvas, never painted) to look at and pick colours from with pick_reference_color. Returns its id, size and placement.",
3366
+ inputSchema: {
3367
+ type: "object",
3368
+ properties: {
3369
+ png: PNG,
3370
+ name: { type: "string", minLength: 1, description: "Display name." },
3371
+ x: { type: "number", description: "Panel left in viewport pixels." },
3372
+ y: { type: "number", description: "Panel top in viewport pixels." },
3373
+ displayWidth: { type: "number", minimum: 16, maximum: 4096, description: "Panel width in screen pixels (default up to 240)." }
3374
+ },
3375
+ required: ["png"],
3376
+ additionalProperties: false
3377
+ },
3378
+ annotations: { title: "Add a reference image", ...WRITE },
3379
+ handler: (a) => refInfo(c.addReferenceImage(a))
3380
+ },
3381
+ {
3382
+ name: "list_reference_images",
3383
+ title: "List reference images",
3384
+ description: "Lists the reference images: id, name, size in pixels, panel placement and visibility.",
3385
+ inputSchema: EMPTY,
3386
+ annotations: { title: "List reference images", ...READ },
3387
+ handler: () => ({ references: guides().references.map(refInfo) })
3388
+ },
3389
+ {
3390
+ name: "update_reference_image",
3391
+ title: "Change a reference image",
3392
+ description: "Moves, resizes, renames, shows or hides a reference image panel. Returns it.",
3393
+ inputSchema: {
3394
+ type: "object",
3395
+ properties: {
3396
+ id: { type: "string", description: "Reference id from list_reference_images." },
3397
+ name: { type: "string", minLength: 1, description: "Display name." },
3398
+ x: { type: "number", description: "Panel left in viewport pixels." },
3399
+ y: { type: "number", description: "Panel top in viewport pixels." },
3400
+ displayWidth: { type: "number", minimum: 16, maximum: 4096, description: "Panel width in screen pixels." },
3401
+ visible: { type: "boolean", description: "Show or hide it." }
3402
+ },
3403
+ required: ["id"],
3404
+ additionalProperties: false
3405
+ },
3406
+ annotations: { title: "Change a reference image", ...SET },
3407
+ handler: ({ id, ...u }) => refInfo(c.updateReferenceImage(id, u))
3408
+ },
3409
+ {
3410
+ name: "remove_reference_image",
3411
+ title: "Remove a reference image",
3412
+ description: "Removes a reference image. Returns the removed id.",
3413
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "Reference id from list_reference_images." } }, required: ["id"], additionalProperties: false },
3414
+ annotations: { title: "Remove a reference image", ...DESTRUCTIVE },
3415
+ handler: ({ id }) => {
3416
+ c.removeReferenceImage(id);
3417
+ return { removed: id };
3418
+ }
3419
+ },
3420
+ {
3421
+ name: "pick_reference_color",
3422
+ title: "Pick a colour from a reference",
3423
+ description: "Reads the colour of a reference image at its own pixel (x, y). With setColor it becomes the paint colour. Returns #rrggbb.",
3424
+ inputSchema: {
3425
+ type: "object",
3426
+ properties: {
3427
+ id: { type: "string", description: "Reference id from list_reference_images." },
3428
+ x: { type: "number", minimum: 0, description: "x in the image\u2019s pixels." },
3429
+ y: { type: "number", minimum: 0, description: "y in the image\u2019s pixels." },
3430
+ setColor: { type: "boolean", description: "Also make it the paint colour (default false)." }
3431
+ },
3432
+ required: ["id", "x", "y"],
3433
+ additionalProperties: false
3434
+ },
3435
+ annotations: { title: "Pick a colour from a reference", ...SET },
3436
+ handler: ({ id, x, y, setColor }) => {
3437
+ const color = c.pickReferenceColor(id, x, y);
3438
+ if (setColor) c.setColor(color);
3439
+ return { color };
3440
+ }
3441
+ },
3442
+ {
3443
+ name: "color_harmony",
3444
+ title: "Colour harmonies",
3445
+ description: "Returns the colours of a harmony around a colour (default the paint colour): complementary, split-complementary, analogous, triadic, tetradic, square or monochromatic, on the painter\u2019s red-yellow-blue wheel (default: red\u2019s complement is green) or the RGB wheel. Changes nothing. Returns the colours with their wheel angles, the base first.",
3446
+ inputSchema: {
3447
+ type: "object",
3448
+ properties: {
3449
+ kind: { type: "string", enum: [...HARMONY_KINDS], description: "The harmony." },
3450
+ color: { ...COLOR, description: "Base colour #rrggbb (default the paint colour)." },
3451
+ wheel: { type: "string", enum: ["ryb", "rgb"], description: "ryb (painter\u2019s wheel, default) or rgb." },
3452
+ spread: { type: "number", minimum: 5, maximum: 90, description: "Analogous and split-complementary spread in degrees (default 30)." }
3453
+ },
3454
+ required: ["kind"],
3455
+ additionalProperties: false
3456
+ },
3457
+ annotations: { title: "Colour harmonies", ...READ },
3458
+ handler: ({ kind, color, wheel, spread }) => ({ kind, wheel: wheel ?? "ryb", colors: c.harmony(kind, { color, wheel, spread }) })
3459
+ },
3460
+ {
3461
+ name: "list_swatches",
3462
+ title: "List swatches",
3463
+ description: "Lists the colour swatches (index, colour, name).",
3464
+ inputSchema: EMPTY,
3465
+ annotations: { title: "List swatches", ...READ },
3466
+ handler: () => ({ swatches: c.getState().swatches.map((s, index) => ({ index, ...s })) })
3467
+ },
3468
+ {
3469
+ name: "add_swatch",
3470
+ title: "Add a swatch",
3471
+ description: "Adds a colour to the swatches (saved with the document). Returns the swatches.",
3472
+ inputSchema: {
3473
+ type: "object",
3474
+ properties: { color: COLOR, name: { type: "string", minLength: 1, description: "Optional name." } },
3475
+ required: ["color"],
3476
+ additionalProperties: false
3477
+ },
3478
+ annotations: { title: "Add a swatch", ...WRITE },
3479
+ handler: ({ color, name }) => ({ swatches: c.addSwatch(color, name).map((s, index) => ({ index, ...s })) })
3480
+ },
3481
+ {
3482
+ name: "remove_swatch",
3483
+ title: "Remove a swatch",
3484
+ description: "Removes a swatch by index (from list_swatches). Returns the swatches.",
3485
+ inputSchema: { type: "object", properties: { index: { type: "integer", minimum: 0, description: "0-based index." } }, required: ["index"], additionalProperties: false },
3486
+ annotations: { title: "Remove a swatch", ...DESTRUCTIVE },
3487
+ handler: ({ index }) => ({ swatches: c.removeSwatch(index).map((s, i) => ({ index: i, ...s })) })
3488
+ },
3489
+ {
3490
+ name: "mixing_pad_stroke",
3491
+ title: "Paint on the mixing pad",
3492
+ description: "Paints on the mixing pad (a 360 \xD7 220 scratch canvas, not the painting): tool paint lays down a colour with a pigment Mixer Brush; tool knife smears what is there like a palette knife, mixing it as paint. Points are pad pixels. Not an undo step. Returns the changed bounds; then pick_mixing_pad_color.",
3493
+ inputSchema: {
3494
+ type: "object",
3495
+ properties: {
3496
+ points: {
3497
+ type: "array",
3498
+ minItems: 1,
3499
+ maxItems: 2e3,
3500
+ description: "The path in pad pixels (0..360, 0..220).",
3501
+ items: {
3502
+ type: "object",
3503
+ properties: { x: { type: "number", description: "Pad x." }, y: { type: "number", description: "Pad y." }, pressure: { type: "number", minimum: 0, maximum: 1, description: "Pressure (default 1)." } },
3504
+ required: ["x", "y"],
3505
+ additionalProperties: false
3506
+ }
3507
+ },
3508
+ tool: { type: "string", enum: ["paint", "knife"], description: "paint (default) or knife." },
3509
+ color: { ...COLOR, description: "Paint colour (default the paint colour; ignored by the knife)." },
3510
+ size: { type: "number", minimum: 1, maximum: 200, description: "Tool size in pad pixels (default 28 paint, 34 knife)." }
3511
+ },
3512
+ required: ["points"],
3513
+ additionalProperties: false
3514
+ },
3515
+ annotations: { title: "Paint on the mixing pad", ...WRITE },
3516
+ handler: ({ points, tool, color, size }) => c.mixingPadStroke(points, { tool, color, size })
3517
+ },
3518
+ {
3519
+ name: "pick_mixing_pad_color",
3520
+ title: "Pick from the mixing pad",
3521
+ description: "Reads the mixed colour on the pad at (x, y), averaged over radius pixels (default 3); with setColor it becomes the paint colour. Returns #rrggbb, or null where the pad is bare.",
3522
+ inputSchema: {
3523
+ type: "object",
3524
+ properties: {
3525
+ x: { type: "number", minimum: 0, maximum: 360, description: "Pad x." },
3526
+ y: { type: "number", minimum: 0, maximum: 220, description: "Pad y." },
3527
+ radius: { type: "integer", minimum: 0, maximum: 20, description: "Averaging radius in pixels (default 3)." },
3528
+ setColor: { type: "boolean", description: "Also make it the paint colour (default false)." }
3529
+ },
3530
+ required: ["x", "y"],
3531
+ additionalProperties: false
3532
+ },
3533
+ annotations: { title: "Pick from the mixing pad", ...SET },
3534
+ handler: ({ x, y, radius, setColor }) => ({ color: c.mixingPadPick(x, y, { radius, setColor }) })
3535
+ },
3536
+ {
3537
+ name: "clear_mixing_pad",
3538
+ title: "Clear the mixing pad",
3539
+ description: "Wipes the mixing pad clean.",
3540
+ inputSchema: EMPTY,
3541
+ annotations: { title: "Clear the mixing pad", ...DESTRUCTIVE },
3542
+ handler: () => {
3543
+ c.clearMixingPad();
3544
+ return { cleared: true };
3545
+ }
3546
+ },
3547
+ {
3548
+ name: "render_mixing_pad",
3549
+ title: "Render the mixing pad",
3550
+ description: "Renders the mixing pad as a PNG (360 \xD7 220, over white). Changes nothing.",
3551
+ inputSchema: EMPTY,
3552
+ annotations: { title: "Render the mixing pad", ...READ },
3553
+ handler: () => {
3554
+ const pad = c.mixingPad;
3555
+ return {
3556
+ content: [
3557
+ { type: "image", data: bytesToBase64(encodePng(pad.width, pad.height, pad.pixels())), mimeType: "image/png" },
3558
+ { type: "text", text: `Mixing pad ${pad.width}\xD7${pad.height}` }
3559
+ ]
3560
+ };
3561
+ }
3562
+ }
3563
+ ];
3564
+ }
3565
+
3566
+ // src/paint-editor/tools.ts
3567
+ var READ2 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3568
+ var WRITE2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
3569
+ var SET2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3570
+ var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
3571
+ var EMPTY2 = { type: "object", properties: {}, additionalProperties: false };
3572
+ var COLOR2 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
2493
3573
  var LAYER_ID = { type: "string", description: "Layer id from list_layers (default the active layer)." };
2494
3574
  var TIP = {
2495
3575
  type: "object",
@@ -2589,8 +3669,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
2589
3669
  name: "get_canvas_info",
2590
3670
  title: "Get the canvas",
2591
3671
  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 },
3672
+ inputSchema: EMPTY2,
3673
+ annotations: { title: "Get the canvas", ...READ2 },
2594
3674
  handler: () => {
2595
3675
  const s = c.getState();
2596
3676
  const d = s.document;
@@ -2627,7 +3707,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2627
3707
  },
2628
3708
  additionalProperties: false
2629
3709
  },
2630
- annotations: { title: "Start a new canvas", ...DESTRUCTIVE },
3710
+ annotations: { title: "Start a new canvas", ...DESTRUCTIVE2 },
2631
3711
  handler: (a) => {
2632
3712
  const d = c.newCanvas({ width: a.width, height: a.height, dpi: a.dpi, paper: a.paper });
2633
3713
  return { width: d.canvas.width, height: d.canvas.height, dpi: d.canvas.dpi, paper: d.paper.preset, layerId: d.activeLayerId };
@@ -2646,15 +3726,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
2646
3726
  },
2647
3727
  additionalProperties: false
2648
3728
  },
2649
- annotations: { title: "Resize the canvas", ...SET },
3729
+ annotations: { title: "Resize the canvas", ...SET2 },
2650
3730
  handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
2651
3731
  });
2652
3732
  add({
2653
3733
  name: "list_papers",
2654
3734
  title: "List papers",
2655
3735
  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 },
3736
+ inputSchema: EMPTY2,
3737
+ annotations: { title: "List papers", ...READ2 },
2658
3738
  handler: () => ({
2659
3739
  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
3740
  families: [...PAPER_FAMILIES],
@@ -2665,8 +3745,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
2665
3745
  name: "get_paper",
2666
3746
  title: "Get the paper",
2667
3747
  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 },
3748
+ inputSchema: EMPTY2,
3749
+ annotations: { title: "Get the paper", ...READ2 },
2670
3750
  handler: () => JSON.parse(JSON.stringify(c.document.paper))
2671
3751
  });
2672
3752
  const tuning2 = {
@@ -2685,7 +3765,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2685
3765
  type: "object",
2686
3766
  properties: {
2687
3767
  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." },
3768
+ color: { ...COLOR2, description: "Paper colour #rrggbb." },
2689
3769
  depth: { type: "number", minimum: 0, maximum: 1, description: "Grain depth 0..1 (0: brushes ignore the grain)." },
2690
3770
  scale: { type: "number", minimum: 0.25, maximum: 8, description: "Grain scale: 1 natural, 2 twice as coarse." },
2691
3771
  seed: { type: "integer", minimum: 0, description: "Grain seed: another sheet of the same paper." },
@@ -2701,15 +3781,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
2701
3781
  },
2702
3782
  additionalProperties: false
2703
3783
  },
2704
- annotations: { title: "Set the paper", ...SET },
3784
+ annotations: { title: "Set the paper", ...SET2 },
2705
3785
  handler: (a) => c.setPaper(a)
2706
3786
  });
2707
3787
  add({
2708
3788
  name: "list_layers",
2709
3789
  title: "List layers",
2710
3790
  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 },
3791
+ inputSchema: EMPTY2,
3792
+ annotations: { title: "List layers", ...READ2 },
2713
3793
  handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
2714
3794
  });
2715
3795
  add({
@@ -2726,7 +3806,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2726
3806
  },
2727
3807
  additionalProperties: false
2728
3808
  },
2729
- annotations: { title: "Add a layer", ...WRITE },
3809
+ annotations: { title: "Add a layer", ...WRITE2 },
2730
3810
  handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
2731
3811
  });
2732
3812
  add({
@@ -2757,7 +3837,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2757
3837
  required: ["layerId"],
2758
3838
  additionalProperties: false
2759
3839
  },
2760
- annotations: { title: "Change a layer", ...SET },
3840
+ annotations: { title: "Change a layer", ...SET2 },
2761
3841
  handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
2762
3842
  });
2763
3843
  add({
@@ -2765,7 +3845,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2765
3845
  title: "Choose the active layer",
2766
3846
  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
3847
  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 },
3848
+ annotations: { title: "Choose the active layer", ...SET2 },
2769
3849
  handler: ({ layerId }) => {
2770
3850
  c.setActiveLayer(layerId);
2771
3851
  return c.describeLayer(c.getLayer(layerId));
@@ -2776,7 +3856,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2776
3856
  title: "Remove a layer",
2777
3857
  description: "Removes a layer (a group with its children). One undo step. Returns the removed id and the new active layer.",
2778
3858
  inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2779
- annotations: { title: "Remove a layer", ...DESTRUCTIVE },
3859
+ annotations: { title: "Remove a layer", ...DESTRUCTIVE2 },
2780
3860
  handler: ({ layerId }) => c.removeLayer(layerId)
2781
3861
  });
2782
3862
  add({
@@ -2784,7 +3864,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2784
3864
  title: "Duplicate a layer",
2785
3865
  description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
2786
3866
  inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2787
- annotations: { title: "Duplicate a layer", ...WRITE },
3867
+ annotations: { title: "Duplicate a layer", ...WRITE2 },
2788
3868
  handler: ({ layerId }) => c.duplicateLayer(layerId)
2789
3869
  });
2790
3870
  add({
@@ -2801,7 +3881,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2801
3881
  required: ["layerId", "index"],
2802
3882
  additionalProperties: false
2803
3883
  },
2804
- annotations: { title: "Reorder a layer", ...WRITE },
3884
+ annotations: { title: "Reorder a layer", ...WRITE2 },
2805
3885
  handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
2806
3886
  });
2807
3887
  add({
@@ -2809,7 +3889,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2809
3889
  title: "Merge a layer down",
2810
3890
  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
3891
  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 },
3892
+ annotations: { title: "Merge a layer down", ...DESTRUCTIVE2 },
2813
3893
  handler: ({ layerId }) => c.mergeDown(layerId)
2814
3894
  });
2815
3895
  add({
@@ -2820,7 +3900,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2820
3900
  type: "object",
2821
3901
  properties: {
2822
3902
  layerId: LAYER_ID,
2823
- color: { ...COLOR, description: "Fill colour #rrggbb (default the paint colour)." },
3903
+ color: { ...COLOR2, description: "Fill colour #rrggbb (default the paint colour)." },
2824
3904
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
2825
3905
  grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
2826
3906
  pressure: { type: "number", minimum: 0, maximum: 1, description: "With grain: how far into the valleys, 0..1 (default 0.5)." },
@@ -2828,7 +3908,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2828
3908
  },
2829
3909
  additionalProperties: false
2830
3910
  },
2831
- annotations: { title: "Fill a layer", ...WRITE },
3911
+ annotations: { title: "Fill a layer", ...WRITE2 },
2832
3912
  handler: (a) => c.fillLayer(a)
2833
3913
  });
2834
3914
  add({
@@ -2836,15 +3916,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
2836
3916
  title: "Clear a layer",
2837
3917
  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
3918
  inputSchema: { type: "object", properties: { layerId: LAYER_ID }, additionalProperties: false },
2839
- annotations: { title: "Clear a layer", ...DESTRUCTIVE },
3919
+ annotations: { title: "Clear a layer", ...DESTRUCTIVE2 },
2840
3920
  handler: ({ layerId }) => c.clearLayer(layerId)
2841
3921
  });
2842
3922
  add({
2843
3923
  name: "list_brushes",
2844
3924
  title: "List brushes",
2845
3925
  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 },
3926
+ inputSchema: EMPTY2,
3927
+ annotations: { title: "List brushes", ...READ2 },
2848
3928
  handler: () => {
2849
3929
  const s = c.getState();
2850
3930
  return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(c.getBrush(s.brush.id)?.builtin), brushes: s.brushes.map(brushSummary) };
@@ -2855,7 +3935,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2855
3935
  title: "Get a brush",
2856
3936
  description: "Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer). Without id, the active brush as edited.",
2857
3937
  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 },
3938
+ annotations: { title: "Get a brush", ...READ2 },
2859
3939
  handler: ({ id }) => {
2860
3940
  if (id === void 0) return brushToJSON(c.getState().brush);
2861
3941
  const b = c.getBrush(id);
@@ -2868,7 +3948,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2868
3948
  title: "Set the brush",
2869
3949
  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
3950
  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 },
3951
+ annotations: { title: "Set the brush", ...SET2 },
2872
3952
  handler: (a) => brushToJSON(c.setBrush(a))
2873
3953
  });
2874
3954
  add({
@@ -2884,7 +3964,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2884
3964
  required: ["id"],
2885
3965
  additionalProperties: false
2886
3966
  },
2887
- annotations: { title: "Save a brush", ...SET },
3967
+ annotations: { title: "Save a brush", ...SET2 },
2888
3968
  handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name }))
2889
3969
  });
2890
3970
  add({
@@ -2892,7 +3972,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2892
3972
  title: "Remove a saved brush",
2893
3973
  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
3974
  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 },
3975
+ annotations: { title: "Remove a saved brush", ...DESTRUCTIVE2 },
2896
3976
  handler: ({ id }) => {
2897
3977
  c.removeBrush(id);
2898
3978
  return { removed: id };
@@ -2907,7 +3987,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2907
3987
  properties: { ids: { type: "array", items: { type: "string", description: "Brush id." }, description: "Brush ids from list_brushes (default every user brush)." } },
2908
3988
  additionalProperties: false
2909
3989
  },
2910
- annotations: { title: "Export brushes", ...READ },
3990
+ annotations: { title: "Export brushes", ...READ2 },
2911
3991
  handler: ({ ids }) => c.exportBrushes(ids)
2912
3992
  });
2913
3993
  add({
@@ -2923,7 +4003,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2923
4003
  required: ["library"],
2924
4004
  additionalProperties: false
2925
4005
  },
2926
- annotations: { title: "Import brushes", ...WRITE },
4006
+ annotations: { title: "Import brushes", ...WRITE2 },
2927
4007
  handler: ({ library, replace }) => c.importBrushes(library, { replace })
2928
4008
  });
2929
4009
  add({
@@ -2936,11 +4016,11 @@ function createPaintEditorToolProvider(controller, options = {}) {
2936
4016
  id: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
2937
4017
  width: { type: "integer", minimum: 16, maximum: 512, description: "Width in pixels (default 160)." },
2938
4018
  height: { type: "integer", minimum: 16, maximum: 512, description: "Height in pixels (default 48)." },
2939
- color: { ...COLOR, description: "Stroke colour (default the paint colour)." }
4019
+ color: { ...COLOR2, description: "Stroke colour (default the paint colour)." }
2940
4020
  },
2941
4021
  additionalProperties: false
2942
4022
  },
2943
- annotations: { title: "Render a brush thumbnail", ...READ },
4023
+ annotations: { title: "Render a brush thumbnail", ...READ2 },
2944
4024
  handler: ({ id, width, height, color }) => {
2945
4025
  const t = c.brushThumbnail(id ?? c.getState().brush.id, { width, height, color });
2946
4026
  return {
@@ -2957,10 +4037,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
2957
4037
  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
4038
  inputSchema: {
2959
4039
  type: "object",
2960
- properties: { color: { ...COLOR, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR, description: "Second colour #rrggbb." } },
4040
+ properties: { color: { ...COLOR2, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR2, description: "Second colour #rrggbb." } },
2961
4041
  additionalProperties: false
2962
4042
  },
2963
- annotations: { title: "Set the paint colour", ...SET },
4043
+ annotations: { title: "Set the paint colour", ...SET2 },
2964
4044
  handler: ({ color, secondaryColor }) => {
2965
4045
  if (color !== void 0) c.setColor(color);
2966
4046
  if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
@@ -2982,23 +4062,23 @@ function createPaintEditorToolProvider(controller, options = {}) {
2982
4062
  required: ["x", "y"],
2983
4063
  additionalProperties: false
2984
4064
  },
2985
- annotations: { title: "Pick a colour", ...READ },
4065
+ annotations: { title: "Pick a colour", ...READ2 },
2986
4066
  handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
2987
4067
  });
2988
4068
  add({
2989
4069
  name: "get_mixer_brush",
2990
4070
  title: "Get the Mixer Brush load",
2991
4071
  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 },
4072
+ inputSchema: EMPTY2,
4073
+ annotations: { title: "Get the Mixer Brush load", ...READ2 },
2994
4074
  handler: () => loadResult(c.getState().mixerLoad)
2995
4075
  });
2996
4076
  add({
2997
4077
  name: "load_mixer_brush",
2998
4078
  title: "Load the Mixer Brush",
2999
4079
  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 },
4080
+ inputSchema: { type: "object", properties: { color: { ...COLOR2, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
4081
+ annotations: { title: "Load the Mixer Brush", ...SET2 },
3002
4082
  handler: ({ color }) => loadResult(c.loadMixerBrush(color))
3003
4083
  });
3004
4084
  add({
@@ -3006,13 +4086,13 @@ function createPaintEditorToolProvider(controller, options = {}) {
3006
4086
  title: "Clean the Mixer Brush",
3007
4087
  description: "Empties the Mixer Brush pickup well (and with emptyReservoir its paint too). Editor state, not an undo step. Returns the load.",
3008
4088
  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 },
4089
+ annotations: { title: "Clean the Mixer Brush", ...SET2 },
3010
4090
  handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
3011
4091
  });
3012
4092
  add({
3013
4093
  name: "paint_stroke",
3014
4094
  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.",
4095
+ 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 symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. 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
4096
  inputSchema: {
3017
4097
  type: "object",
3018
4098
  properties: {
@@ -3020,15 +4100,16 @@ function createPaintEditorToolProvider(controller, options = {}) {
3020
4100
  layerId: LAYER_ID,
3021
4101
  brushId: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
3022
4102
  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." }
4103
+ color: { ...COLOR2, description: "Paint colour #rrggbb (default the paint colour)." },
4104
+ seed: { type: "integer", minimum: 0, description: "Jitter seed (default a new one); the same seed and points give the same marks." },
4105
+ snapToPerspective: { type: "boolean", description: "Snap the points to the perspective guide, as a pen stroke would (default false)." }
3025
4106
  },
3026
4107
  required: ["points"],
3027
4108
  additionalProperties: false
3028
4109
  },
3029
- annotations: { title: "Paint a brush stroke", ...WRITE },
4110
+ annotations: { title: "Paint a brush stroke", ...WRITE2 },
3030
4111
  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 });
4112
+ const r = c.paintStroke({ points: a.points, layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, seed: a.seed, snapToPerspective: a.snapToPerspective });
3032
4113
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
3033
4114
  }
3034
4115
  });
@@ -3044,7 +4125,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3044
4125
  },
3045
4126
  additionalProperties: false
3046
4127
  },
3047
- annotations: { title: "List recorded strokes", ...READ },
4128
+ annotations: { title: "List recorded strokes", ...READ2 },
3048
4129
  handler: ({ ids, last }) => {
3049
4130
  const all = c.getStrokes();
3050
4131
  const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(last ?? 100));
@@ -3093,7 +4174,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3093
4174
  additionalProperties: false
3094
4175
  },
3095
4176
  pressureScale: { type: "number", minimum: 0, maximum: 4, description: "Multiply every pressure (default 1)." },
3096
- color: { ...COLOR, description: "Recolour (default each stroke\u2019s colour)." },
4177
+ color: { ...COLOR2, description: "Recolour (default each stroke\u2019s colour)." },
3097
4178
  brushId: { type: "string", description: "Replay with this brush from list_brushes (default each stroke\u2019s brush)." },
3098
4179
  layerId: { type: "string", description: "Paint on this layer (default each stroke\u2019s layer)." },
3099
4180
  repeat: { type: "integer", minimum: 1, maximum: 100, description: "How many copies (default 1)." }
@@ -3101,7 +4182,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3101
4182
  required: ["strokeIds"],
3102
4183
  additionalProperties: false
3103
4184
  },
3104
- annotations: { title: "Replay strokes, varied", ...WRITE },
4185
+ annotations: { title: "Replay strokes, varied", ...WRITE2 },
3105
4186
  handler: (a) => c.replayStrokes(a)
3106
4187
  });
3107
4188
  add({
@@ -3118,7 +4199,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3118
4199
  },
3119
4200
  additionalProperties: false
3120
4201
  },
3121
- annotations: { title: "Rotate, mirror or zoom the view", ...SET },
4202
+ annotations: { title: "Rotate, mirror or zoom the view", ...SET2 },
3122
4203
  handler: ({ reset, ...v }) => {
3123
4204
  if (reset) c.resetView();
3124
4205
  return c.setView(v);
@@ -3129,15 +4210,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
3129
4210
  title: "Set the drawing hand",
3130
4211
  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
4212
  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 },
4213
+ annotations: { title: "Set the drawing hand", ...SET2 },
3133
4214
  handler: ({ hand }) => ({ hand: c.setHand(hand) })
3134
4215
  });
3135
4216
  add({
3136
4217
  name: "undo_paint",
3137
4218
  title: "Undo",
3138
4219
  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 },
4220
+ inputSchema: EMPTY2,
4221
+ annotations: { title: "Undo", ...WRITE2 },
3141
4222
  handler: () => {
3142
4223
  const undone = c.undo();
3143
4224
  const s = c.getState();
@@ -3148,8 +4229,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
3148
4229
  name: "redo_paint",
3149
4230
  title: "Redo",
3150
4231
  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 },
4232
+ inputSchema: EMPTY2,
4233
+ annotations: { title: "Redo", ...WRITE2 },
3153
4234
  handler: () => {
3154
4235
  const redone = c.redo();
3155
4236
  const s = c.getState();
@@ -3169,7 +4250,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3169
4250
  },
3170
4251
  additionalProperties: false
3171
4252
  },
3172
- annotations: { title: "Render a preview", ...READ },
4253
+ annotations: { title: "Render a preview", ...READ2 },
3173
4254
  handler: ({ maxSide, layerIds, paper }) => {
3174
4255
  if (layerIds) {
3175
4256
  for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
@@ -3192,7 +4273,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3192
4273
  },
3193
4274
  additionalProperties: false
3194
4275
  },
3195
- annotations: { title: "Export the painting", ...READ },
4276
+ annotations: { title: "Export the painting", ...READ2 },
3196
4277
  handler: async ({ format, quality, scale, paper }) => {
3197
4278
  const bytes = await c.exportImage({ format, quality, scale, paper });
3198
4279
  return { mimeType: format ?? "image/png", bytes: bytes.length, data: bytesToBase64(bytes) };
@@ -3202,8 +4283,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
3202
4283
  name: "export_psd",
3203
4284
  title: "Export as PSD",
3204
4285
  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 },
4286
+ inputSchema: EMPTY2,
4287
+ annotations: { title: "Export as PSD", ...READ2 },
3207
4288
  handler: () => {
3208
4289
  const r = c.exportPsd();
3209
4290
  return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
@@ -3214,7 +4295,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3214
4295
  title: "Save the document",
3215
4296
  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
4297
  inputSchema: { type: "object", properties: { strokes: { type: "boolean", description: "Include the recorded strokes (default true)." } }, additionalProperties: false },
3217
- annotations: { title: "Save the document", ...READ },
4298
+ annotations: { title: "Save the document", ...READ2 },
3218
4299
  handler: ({ strokes }) => c.toJSON({ strokes })
3219
4300
  });
3220
4301
  add({
@@ -3222,72 +4303,376 @@ function createPaintEditorToolProvider(controller, options = {}) {
3222
4303
  title: "Open a document",
3223
4304
  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
4305
  inputSchema: { type: "object", properties: { document: { type: "object", description: "The saved document JSON." } }, required: ["document"], additionalProperties: false },
3225
- annotations: { title: "Open a document", ...DESTRUCTIVE },
4306
+ annotations: { title: "Open a document", ...DESTRUCTIVE2 },
3226
4307
  handler: ({ document }) => {
3227
4308
  const problems = c.loadJSON(document);
3228
4309
  const d = c.document;
3229
4310
  return { width: d.canvas.width, height: d.canvas.height, layers: c.listLayers(), strokes: d.strokes.length, problems };
3230
4311
  }
3231
4312
  });
4313
+ tools.push(...createPaintGuideTools(c));
3232
4314
  tools.push(...createPigmentTools());
3233
4315
  return { id: options.id ?? "paint-editor", namespace: options.namespace, tools };
3234
4316
  }
4317
+
4318
+ // src/paint-editor/ui.ts
4319
+ var PAINT_TOOLS = [
4320
+ { id: "brush", label: "Brush", key: "b" },
4321
+ { id: "eyedropper", label: "Eyedropper", key: "i" },
4322
+ { id: "pan", label: "Pan", key: "h" },
4323
+ { id: "rotate", label: "Rotate view", key: "r" }
4324
+ ];
4325
+ var PaintGesture = class {
4326
+ constructor(controller, box) {
4327
+ this.controller = controller;
4328
+ this.box = box;
4329
+ }
4330
+ controller;
4331
+ box;
4332
+ tool = "brush";
4333
+ /** The last refusal (a hidden or locked layer), for the editor to show. */
4334
+ error = null;
4335
+ /** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */
4336
+ predicted = [];
4337
+ mode = null;
4338
+ penAt = null;
4339
+ penDown = false;
4340
+ doc(e) {
4341
+ const b = this.box();
4342
+ const s = this.controller.getState();
4343
+ return screenToDocument(s.view, s.document.canvas, b, e.clientX - b.left, e.clientY - b.top);
4344
+ }
4345
+ pen(e) {
4346
+ const p = this.doc(e);
4347
+ return pointerToPenPoint(e, p.x, p.y, this.controller.getState().view);
4348
+ }
4349
+ /** Pointer down; returns true when the gesture took the pointer (capture it). */
4350
+ down(e) {
4351
+ this.error = null;
4352
+ if (this.mode) return false;
4353
+ if (e.pointerType === "pen") {
4354
+ this.penDown = true;
4355
+ this.penAt = { x: e.clientX, y: e.clientY };
4356
+ } else if (e.pointerType === "touch" && (this.penDown || isPalmTouch({ x: e.clientX, y: e.clientY }, this.penAt, this.controller.getState().hand))) {
4357
+ return false;
4358
+ }
4359
+ const middle = e.button === 1;
4360
+ const tool = middle ? "pan" : this.tool;
4361
+ if (tool === "pan") {
4362
+ this.mode = { kind: "pan", pointerId: e.pointerId, x: e.clientX, y: e.clientY, pan: { ...this.controller.getState().view.pan } };
4363
+ return true;
4364
+ }
4365
+ if (tool === "rotate") {
4366
+ const b = this.box();
4367
+ this.mode = { kind: "rotate", pointerId: e.pointerId, angle: Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2), rotation: this.controller.getState().view.rotation };
4368
+ return true;
4369
+ }
4370
+ if (tool === "eyedropper" || e.altKey) {
4371
+ this.pick(e);
4372
+ return false;
4373
+ }
4374
+ try {
4375
+ const p = this.pen(e);
4376
+ const session = this.controller.beginStroke({ start: p });
4377
+ session.push([p]);
4378
+ this.mode = { kind: "stroke", pointerId: e.pointerId };
4379
+ return true;
4380
+ } catch (err) {
4381
+ this.error = err instanceof Error ? err.message : String(err);
4382
+ return false;
4383
+ }
4384
+ }
4385
+ pick(e) {
4386
+ const p = this.doc(e);
4387
+ const { width, height } = this.controller.document.canvas;
4388
+ if (p.x < 0 || p.y < 0 || p.x >= width || p.y >= height) return;
4389
+ this.controller.setColor(this.controller.pickColor(p.x, p.y).color);
4390
+ }
4391
+ move(e) {
4392
+ if (e.pointerType === "pen") this.penAt = { x: e.clientX, y: e.clientY };
4393
+ const m = this.mode;
4394
+ if (!m || m.pointerId !== e.pointerId) return;
4395
+ if (m.kind === "pan") {
4396
+ this.controller.setView({ pan: { x: m.pan.x + e.clientX - m.x, y: m.pan.y + e.clientY - m.y } });
4397
+ return;
4398
+ }
4399
+ if (m.kind === "rotate") {
4400
+ const b = this.box();
4401
+ const a = Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2);
4402
+ let r = m.rotation + (a - m.angle) * 180 / Math.PI;
4403
+ if (e.shiftKey) r = Math.round(r / 15) * 15;
4404
+ this.controller.setView({ rotation: r });
4405
+ return;
4406
+ }
4407
+ const session = this.controller.activeStroke;
4408
+ if (!session) return;
4409
+ const events = typeof e.getCoalescedEvents === "function" ? e.getCoalescedEvents() : [];
4410
+ session.push((events.length ? events : [e]).map((ev) => this.pen(ev)));
4411
+ const predicted = typeof e.getPredictedEvents === "function" ? e.getPredictedEvents() : [];
4412
+ this.predicted = predicted.map((ev) => this.pen(ev));
4413
+ }
4414
+ up(e) {
4415
+ if (e.pointerType === "pen") this.penDown = false;
4416
+ const m = this.mode;
4417
+ if (!m || m.pointerId !== e.pointerId) return;
4418
+ this.mode = null;
4419
+ this.predicted = [];
4420
+ if (m.kind === "stroke") this.controller.activeStroke?.end();
4421
+ }
4422
+ cancel() {
4423
+ this.penDown = false;
4424
+ this.predicted = [];
4425
+ if (this.mode?.kind === "stroke") this.controller.activeStroke?.cancel();
4426
+ this.mode = null;
4427
+ }
4428
+ get active() {
4429
+ return this.mode !== null;
4430
+ }
4431
+ /** Wheel: zoom about the pointer (Ctrl / pinch or plain wheel), Shift+wheel rotates the view. */
4432
+ wheel(e) {
4433
+ const s = this.controller.getState();
4434
+ if (e.shiftKey) {
4435
+ this.controller.setView({ rotation: s.view.rotation + (e.deltaY > 0 ? 5 : -5) });
4436
+ return;
4437
+ }
4438
+ const b = this.box();
4439
+ const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, s.view.zoom * Math.exp(-e.deltaY * 15e-4)));
4440
+ const k = zoom / s.view.zoom;
4441
+ const px = e.clientX - b.left - b.width / 2;
4442
+ const py = e.clientY - b.top - b.height / 2;
4443
+ this.controller.setView({ zoom, pan: { x: px - (px - s.view.pan.x) * k, y: py - (py - s.view.pan.y) * k } });
4444
+ }
4445
+ };
4446
+ function handlePaintShortcut(controller, gesture, e) {
4447
+ const mod = e.ctrlKey || e.metaKey;
4448
+ const key = e.key.toLowerCase();
4449
+ if (mod && key === "z") {
4450
+ if (controller.getState().stroking) return true;
4451
+ if (e.shiftKey) controller.redo();
4452
+ else controller.undo();
4453
+ return true;
4454
+ }
4455
+ if (mod && key === "y") {
4456
+ controller.redo();
4457
+ return true;
4458
+ }
4459
+ if (mod) return false;
4460
+ const tool = PAINT_TOOLS.find((t) => t.key === key);
4461
+ if (tool) {
4462
+ gesture.tool = tool.id;
4463
+ return true;
4464
+ }
4465
+ const brush = controller.getState().brush;
4466
+ if (key === "[" || key === "]") {
4467
+ const step = Math.max(1, Math.round(brush.tip.size * 0.15));
4468
+ controller.setBrush({ tip: { size: Math.max(1, brush.tip.size + (key === "]" ? step : -step)) } });
4469
+ return true;
4470
+ }
4471
+ if (key === "x") {
4472
+ controller.swapColors();
4473
+ return true;
4474
+ }
4475
+ if (key === "m") {
4476
+ controller.setView({ mirror: !controller.getState().view.mirror });
4477
+ return true;
4478
+ }
4479
+ if (key === "escape") {
4480
+ controller.resetView();
4481
+ return true;
4482
+ }
4483
+ return false;
4484
+ }
4485
+ var PaintCanvasPresenter = class {
4486
+ constructor(controller) {
4487
+ this.controller = controller;
4488
+ }
4489
+ controller;
4490
+ uploaded = /* @__PURE__ */ new Map();
4491
+ scratch = null;
4492
+ size = "";
4493
+ /** Draws what changed into `ctx` (a canvas the size of the document); returns the tiles drawn. */
4494
+ render(ctx) {
4495
+ const { width, height } = this.controller.document.canvas;
4496
+ const size = `${width}x${height}`;
4497
+ let dirty = this.controller.takeDirty();
4498
+ if (size !== this.size) {
4499
+ this.size = size;
4500
+ this.uploaded.clear();
4501
+ dirty = "all";
4502
+ }
4503
+ let drawn = 0;
4504
+ const rect = { x: 0, y: 0, width, height };
4505
+ this.controller.compositeTiles(rect, (tx, ty, data) => {
4506
+ const key = `${tx},${ty}`;
4507
+ if (this.uploaded.get(key) === data) return;
4508
+ this.uploaded.set(key, data);
4509
+ if (!this.scratch) this.scratch = ctx.createImageData(TILE, TILE);
4510
+ this.scratch.data.set(data);
4511
+ const ox = tx * TILE;
4512
+ const oy = ty * TILE;
4513
+ ctx.putImageData(this.scratch, ox, oy, 0, 0, Math.min(TILE, width - ox), Math.min(TILE, height - oy));
4514
+ drawn++;
4515
+ }, { dirty });
4516
+ return drawn;
4517
+ }
4518
+ };
4519
+ function guideOverlay(controller) {
4520
+ const s = controller.getState();
4521
+ const size = s.document.canvas;
4522
+ const out = [];
4523
+ for (const [from, to] of symmetryAxisSegments(s.guides.symmetry, size)) out.push({ from, to, kind: "symmetry" });
4524
+ if (s.guides.perspective.enabled) for (const [from, to] of perspectiveGuideLines(s.guides.perspective, size, 16)) out.push({ from, to, kind: "perspective" });
4525
+ return out;
4526
+ }
4527
+ var BRUSH_SLIDERS = [
4528
+ { path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
4529
+ { path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
4530
+ { path: "tip.flow", label: "Flow", min: 0, max: 1, step: 0.01 },
4531
+ { path: "tip.hardness", label: "Hardness", min: 0, max: 1, step: 0.01 },
4532
+ { path: "tip.spacing", label: "Spacing", min: 0.01, max: 1, step: 0.01 },
4533
+ { path: "tip.roundness", label: "Roundness", min: 0.05, max: 1, step: 0.01 },
4534
+ { path: "tip.angle", label: "Angle", min: -180, max: 180, step: 1 },
4535
+ { path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 },
4536
+ { path: "tip.smoothing", label: "Smoothing", min: 0, max: 0.95, step: 0.01 },
4537
+ { path: "pressureSmoothing", label: "Pressure smoothing", min: 0, max: 0.95, step: 0.01 },
4538
+ { path: "dynamics.minSize", label: "Min size", min: 0, max: 1, step: 0.01 },
4539
+ { path: "dynamics.tiltSize", label: "Tilt size", min: 0, max: 1, step: 0.01 },
4540
+ { path: "dynamics.velocitySize", label: "Speed size", min: -1, max: 1, step: 0.01 }
4541
+ ];
4542
+ var PAPER_SLIDERS = [
4543
+ { path: "depth", label: "Grain depth", min: 0, max: 1, step: 0.01 },
4544
+ { path: "scale", label: "Grain scale", min: 0.25, max: 8, step: 0.05 },
4545
+ { path: "shading", label: "Shading", min: 0, max: 1, step: 0.01 },
4546
+ { path: "dry.contrast", label: "Contrast", min: 0, max: 3, step: 0.05 },
4547
+ { path: "absorption", label: "Absorption", min: 0, max: 1, step: 0.01 },
4548
+ { path: "permeability", label: "Permeability", min: 0, max: 1, step: 0.01 },
4549
+ { path: "roughness", label: "Roughness", min: 0, max: 1, step: 0.01 }
4550
+ ];
4551
+ function readPath(obj, path) {
4552
+ let v = obj;
4553
+ for (const k of path.split(".")) v = v?.[k];
4554
+ return typeof v === "number" ? v : 0;
4555
+ }
4556
+ function patchPath(obj, path, value) {
4557
+ const [head, ...rest] = path.split(".");
4558
+ if (!rest.length) return { [head]: value };
4559
+ return { [head]: { ...obj[head], ...patchPath(obj[head] ?? {}, rest.join("."), value) } };
4560
+ }
4561
+ function wheelPoint(angle, radius) {
4562
+ const a = (angle - 90) * Math.PI / 180;
4563
+ return { x: Math.cos(a) * radius, y: Math.sin(a) * radius };
4564
+ }
4565
+ function wheelAngle(x, y) {
4566
+ return (Math.atan2(y, x) * 180 / Math.PI + 90 + 360) % 360;
4567
+ }
4568
+ function renderColorWheel(size, wheel, inner = 0.64) {
4569
+ const data = new Uint8ClampedArray(size * size * 4);
4570
+ const r = size / 2;
4571
+ for (let y = 0; y < size; y++) {
4572
+ for (let x = 0; x < size; x++) {
4573
+ const dx = x + 0.5 - r;
4574
+ const dy = y + 0.5 - r;
4575
+ const d = Math.hypot(dx, dy) / r;
4576
+ if (d > 1 || d < inner) continue;
4577
+ const [cr, cg, cb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), 1, 1);
4578
+ const i = (y * size + x) * 4;
4579
+ const edge = Math.min(1, (1 - d) * r, (d - inner) * r);
4580
+ data[i] = cr;
4581
+ data[i + 1] = cg;
4582
+ data[i + 2] = cb;
4583
+ data[i + 3] = Math.round(255 * Math.max(0, edge));
4584
+ }
4585
+ }
4586
+ return { width: size, height: size, data };
4587
+ }
4588
+ function wheelPick(dx, dy, wheel, color) {
4589
+ const [r, g, b] = parseHexColor(color);
4590
+ const { s, v } = rgbToHsv(r, g, b);
4591
+ const [nr, ng, nb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), s || 1, v || 1);
4592
+ return rgbToHexColor(nr, ng, nb);
4593
+ }
3235
4594
  export {
4595
+ BLEND_MODES,
3236
4596
  BRUSH_ID,
3237
4597
  BRUSH_ROTATIONS,
4598
+ BRUSH_SLIDERS,
3238
4599
  BUILTIN_BRUSHES,
3239
4600
  DEFAULT_BRUSH_ID,
3240
4601
  DEFAULT_DPI,
3241
4602
  DEFAULT_DYNAMICS,
3242
4603
  DEFAULT_PAPER,
4604
+ DEFAULT_SWATCHES,
3243
4605
  DEFAULT_VIEW,
3244
4606
  DryStroke,
3245
4607
  GRAIN_PATTERNS,
3246
4608
  GRAIN_TILE,
4609
+ HARMONY_KINDS,
3247
4610
  IDENTITY_AFFINE,
3248
4611
  IDENTITY_COPY,
3249
4612
  MAX_BRUSH_SIZE,
3250
4613
  MAX_CANVAS_SIDE,
4614
+ MAX_COORDINATE,
4615
+ MAX_IMAGE_SIDE,
3251
4616
  MAX_ZOOM,
3252
4617
  MIN_CANVAS_SIDE,
3253
4618
  MIN_ZOOM,
4619
+ MIXING_PAD_HEIGHT,
4620
+ MIXING_PAD_WIDTH,
4621
+ MixingPad,
3254
4622
  PAINT_BRUSH_CATEGORIES,
3255
4623
  PAINT_BRUSH_ENGINES,
3256
4624
  PAINT_FORMAT,
3257
4625
  PAINT_SCHEMA_VERSION,
4626
+ PAINT_TOOLS,
3258
4627
  PAPER_FAMILIES,
3259
4628
  PAPER_LIBRARY,
3260
4629
  PAPER_NODE_ID,
4630
+ PAPER_SLIDERS,
4631
+ PaintCanvasPresenter,
3261
4632
  PaintDocumentError,
3262
4633
  PaintEditorController,
3263
4634
  PaintEditorError,
4635
+ PaintGesture,
3264
4636
  PaintStrokeSession,
3265
4637
  PaperGrain,
3266
4638
  PenDabWalker,
3267
4639
  PenSmoother,
4640
+ PerspectiveSnapper,
4641
+ SNAP_DECIDE_DISTANCE,
4642
+ SYMMETRY_MODES,
3268
4643
  TRACING_NODE_ID,
3269
4644
  TileableNoise,
3270
4645
  affineCopy,
3271
4646
  applyAffine,
3272
4647
  brushToJSON,
3273
4648
  clonePaper,
4649
+ colorHarmony,
3274
4650
  composeAffine,
3275
4651
  createPaintCompositor,
3276
4652
  createPaintDocument,
3277
4653
  createPaintEditorToolProvider,
3278
4654
  createPaintLayer,
4655
+ decodeImageBlob,
4656
+ defaultGuides,
3279
4657
  defaultImageEncoder,
3280
4658
  deserializePaintDocument,
3281
4659
  deserializePaintDocumentAsync,
3282
4660
  documentExtensions,
3283
4661
  documentToScreen,
4662
+ downloadBytes,
3284
4663
  downsampleRgba,
3285
4664
  fitZoom,
3286
4665
  grainCatch,
3287
4666
  grainHeights,
4667
+ guideOverlay,
4668
+ handlePaintShortcut,
3288
4669
  hash2,
3289
4670
  hexToRgb8,
4671
+ hslToRgb,
4672
+ hsvToRgb,
4673
+ hueToWheel,
3290
4674
  invertAffine,
4675
+ isBlendMode,
3291
4676
  isPalmTouch,
3292
4677
  mapPenPoint,
3293
4678
  mix32,
@@ -3299,25 +4684,39 @@ export {
3299
4684
  normalizeDynamics,
3300
4685
  normalizePaintBrush,
3301
4686
  normalizePaper,
4687
+ normalizePerspective,
3302
4688
  normalizeRotation,
4689
+ normalizeSymmetry,
3303
4690
  normalizeView,
3304
4691
  paperGrain,
3305
4692
  paperKey,
3306
4693
  paperNode,
3307
4694
  paperPreset,
3308
4695
  parseBrushLibrary,
4696
+ parseHexColor,
4697
+ patchPath,
3309
4698
  penOrientation,
4699
+ perspectiveDirections,
4700
+ perspectiveGuideLines,
3310
4701
  pointerToPenPoint,
3311
4702
  rand01,
3312
4703
  randSigned,
4704
+ readPath,
3313
4705
  renderBrushThumbnail,
4706
+ renderColorWheel,
3314
4707
  renderPaper,
4708
+ rgbToHexColor,
4709
+ rgbToHsl,
4710
+ rgbToHsv,
3315
4711
  samplePressure,
3316
4712
  screenToDocument,
3317
4713
  seedFromString,
3318
4714
  serializePaintDocument,
3319
4715
  serializePaintDocumentExternal,
3320
4716
  strokeBounds,
4717
+ symmetryAxisAngles,
4718
+ symmetryAxisSegments,
4719
+ symmetryCopies,
3321
4720
  thumbnailStroke,
3322
4721
  tiltToOrientation,
3323
4722
  toStrokePoint,
@@ -3325,6 +4724,10 @@ export {
3325
4724
  varyStroke,
3326
4725
  viewCssTransform,
3327
4726
  viewMatrix,
3328
- viewPenToDocument
4727
+ viewPenToDocument,
4728
+ wheelAngle,
4729
+ wheelPick,
4730
+ wheelPoint,
4731
+ wheelToHue
3329
4732
  };
3330
4733
  //# sourceMappingURL=index.js.map