@pixerate/editor 0.103.0 → 0.104.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.
@@ -8,12 +8,15 @@ import {
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.");
@@ -1562,6 +1570,411 @@ function renderBrushThumbnail(brush, options) {
1562
1570
  return { width, height, data };
1563
1571
  }
1564
1572
 
1573
+ // src/paint-editor/guides.ts
1574
+ var SYMMETRY_MODES = ["off", "vertical", "horizontal", "dual", "diagonal", "kaleidoscope"];
1575
+ function defaultGuides(canvas) {
1576
+ return {
1577
+ symmetry: { mode: "off", center: { x: canvas.width / 2, y: canvas.height / 2 }, rotation: 0, segments: 6, mirror: true },
1578
+ perspective: { enabled: false, points: [], snap: true },
1579
+ tracing: null,
1580
+ references: []
1581
+ };
1582
+ }
1583
+ var MAX_COORDINATE = 1e6;
1584
+ var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
1585
+ var coord = (v, d) => finite2(v) && Math.abs(v) <= MAX_COORDINATE ? v : d;
1586
+ function normalizeSymmetry(raw, base) {
1587
+ const r = raw;
1588
+ const c = r.center ?? {};
1589
+ return {
1590
+ mode: SYMMETRY_MODES.includes(r.mode) ? r.mode : base.mode,
1591
+ center: { x: coord(c.x, base.center.x), y: coord(c.y, base.center.y) },
1592
+ rotation: finite2(r.rotation) ? (r.rotation % 360 + 360) % 360 : base.rotation,
1593
+ segments: finite2(r.segments) ? Math.min(32, Math.max(2, Math.round(r.segments))) : base.segments,
1594
+ mirror: typeof r.mirror === "boolean" ? r.mirror : base.mirror
1595
+ };
1596
+ }
1597
+ function reflectAbout(c, a) {
1598
+ const dx = Math.cos(a);
1599
+ const dy = Math.sin(a);
1600
+ const m00 = 2 * dx * dx - 1;
1601
+ const m01 = 2 * dx * dy;
1602
+ const m11 = 2 * dy * dy - 1;
1603
+ return [m00, m01, m01, m11, c.x - (m00 * c.x + m01 * c.y), c.y - (m01 * c.x + m11 * c.y)];
1604
+ }
1605
+ function rotateAbout(c, a) {
1606
+ const cos = Math.cos(a);
1607
+ const sin = Math.sin(a);
1608
+ return [cos, -sin, sin, cos, c.x - (cos * c.x - sin * c.y), c.y - (sin * c.x + cos * c.y)];
1609
+ }
1610
+ var rad = (deg) => deg * Math.PI / 180;
1611
+ function symmetryAxisAngles(g) {
1612
+ if (g.mode === "off") return [];
1613
+ if (g.mode === "kaleidoscope") return Array.from({ length: g.segments }, (_, k) => rad(g.rotation) + k * Math.PI / g.segments);
1614
+ const initial = g.mode === "horizontal" ? 0 : g.mode === "diagonal" ? 45 : 90;
1615
+ const a = rad(initial + g.rotation);
1616
+ return g.mode === "dual" ? [a, a + Math.PI / 2] : [a];
1617
+ }
1618
+ function symmetryCopies(g) {
1619
+ const c = g.center;
1620
+ switch (g.mode) {
1621
+ case "off":
1622
+ return [];
1623
+ case "vertical":
1624
+ case "horizontal":
1625
+ case "diagonal":
1626
+ return symmetryAxisAngles(g).map((a) => reflectAbout(c, a));
1627
+ case "dual": {
1628
+ const [a, b] = symmetryAxisAngles(g);
1629
+ return [reflectAbout(c, a), reflectAbout(c, b), rotateAbout(c, Math.PI)];
1630
+ }
1631
+ case "kaleidoscope": {
1632
+ const out = [];
1633
+ const n = g.segments;
1634
+ for (let k = 1; k < n; k++) out.push(rotateAbout(c, 2 * Math.PI * k / n));
1635
+ if (g.mirror) {
1636
+ const first = reflectAbout(c, rad(g.rotation));
1637
+ for (let k = 0; k < n; k++) out.push(composeAffine(rotateAbout(c, 2 * Math.PI * k / n), first));
1638
+ }
1639
+ return out;
1640
+ }
1641
+ }
1642
+ }
1643
+ function symmetryAxisSegments(g, size) {
1644
+ const out = [];
1645
+ for (const a of symmetryAxisAngles(g)) {
1646
+ const d = [Math.cos(a), Math.sin(a)];
1647
+ const origin = [g.center.x, g.center.y];
1648
+ const extent = [size.width, size.height];
1649
+ let lo = -Infinity;
1650
+ let hi = Infinity;
1651
+ let ok = true;
1652
+ for (let i = 0; i < 2; i++) {
1653
+ if (Math.abs(d[i]) < 1e-12) {
1654
+ if (origin[i] < 0 || origin[i] > extent[i]) ok = false;
1655
+ } else {
1656
+ const t0 = -origin[i] / d[i];
1657
+ const t1 = (extent[i] - origin[i]) / d[i];
1658
+ lo = Math.max(lo, Math.min(t0, t1));
1659
+ hi = Math.min(hi, Math.max(t0, t1));
1660
+ }
1661
+ }
1662
+ if (!ok || !Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo) continue;
1663
+ 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] }]);
1664
+ }
1665
+ return out;
1666
+ }
1667
+ function normalizePerspective(raw, base) {
1668
+ const r = raw;
1669
+ let points = base.points;
1670
+ if (Array.isArray(r.points)) {
1671
+ if (r.points.length > 3) throw new Error(`A perspective guide has one to three vanishing points; got ${r.points.length}.`);
1672
+ points = r.points.map((p, i) => {
1673
+ const q = p ?? {};
1674
+ if (!finite2(q.x) || !finite2(q.y) || Math.abs(q.x) > MAX_COORDINATE || Math.abs(q.y) > MAX_COORDINATE) {
1675
+ throw new Error(`points[${i}] needs finite x and y within \xB1${MAX_COORDINATE} document pixels.`);
1676
+ }
1677
+ return { x: q.x, y: q.y };
1678
+ });
1679
+ }
1680
+ const enabled = typeof r.enabled === "boolean" ? r.enabled : base.enabled;
1681
+ if (enabled && !points.length) throw new Error("An enabled perspective guide needs at least one vanishing point; pass points.");
1682
+ return { enabled, points, snap: typeof r.snap === "boolean" ? r.snap : base.snap };
1683
+ }
1684
+ function perspectiveDirections(g, p) {
1685
+ const out = [];
1686
+ for (const v of g.points) {
1687
+ const dx = v.x - p.x;
1688
+ const dy = v.y - p.y;
1689
+ const len = Math.hypot(dx, dy);
1690
+ if (len > 1e-6) out.push({ x: dx / len, y: dy / len });
1691
+ }
1692
+ if (g.points.length <= 2) out.push({ x: 0, y: 1 });
1693
+ if (g.points.length <= 1) out.push({ x: 1, y: 0 });
1694
+ return out;
1695
+ }
1696
+ var SNAP_DECIDE_DISTANCE = 6;
1697
+ var PerspectiveSnapper = class {
1698
+ constructor(guide) {
1699
+ this.guide = guide;
1700
+ }
1701
+ guide;
1702
+ start = null;
1703
+ dir = null;
1704
+ /** The line the stroke follows, once chosen. */
1705
+ get line() {
1706
+ return this.start && this.dir ? { from: this.start, direction: this.dir } : null;
1707
+ }
1708
+ snap(p) {
1709
+ if (!this.guide.enabled || !this.guide.snap || !this.guide.points.length) return p;
1710
+ if (!this.start) {
1711
+ this.start = { x: p.x, y: p.y };
1712
+ return p;
1713
+ }
1714
+ const s = this.start;
1715
+ if (!this.dir) {
1716
+ const dx = p.x - s.x;
1717
+ const dy = p.y - s.y;
1718
+ const len = Math.hypot(dx, dy);
1719
+ if (len < SNAP_DECIDE_DISTANCE) return { ...p, x: s.x, y: s.y };
1720
+ let best = null;
1721
+ let bestDot = -1;
1722
+ for (const d2 of perspectiveDirections(this.guide, s)) {
1723
+ const dot = Math.abs((dx * d2.x + dy * d2.y) / len);
1724
+ if (dot > bestDot) {
1725
+ bestDot = dot;
1726
+ best = d2;
1727
+ }
1728
+ }
1729
+ this.dir = best;
1730
+ if (!best) return p;
1731
+ }
1732
+ const d = this.dir;
1733
+ const t = (p.x - s.x) * d.x + (p.y - s.y) * d.y;
1734
+ return { ...p, x: s.x + d.x * t, y: s.y + d.y * t };
1735
+ }
1736
+ };
1737
+ function perspectiveGuideLines(g, size, count = 12) {
1738
+ const out = [];
1739
+ const far = Math.hypot(size.width, size.height) * 4;
1740
+ for (const v of g.points) {
1741
+ const cx = size.width / 2 - v.x;
1742
+ const cy = size.height / 2 - v.y;
1743
+ const inside = v.x >= 0 && v.y >= 0 && v.x <= size.width && v.y <= size.height;
1744
+ const mid = Math.atan2(cy, cx);
1745
+ const spread = inside ? Math.PI : Math.min(Math.PI, 2 * Math.atan2(Math.hypot(size.width, size.height) / 2, Math.hypot(cx, cy)));
1746
+ for (let i = 0; i < count; i++) {
1747
+ const a = inside ? 2 * Math.PI * i / count : mid - spread / 2 + spread * (i + 0.5) / count;
1748
+ out.push([{ ...v }, { x: v.x + Math.cos(a) * far, y: v.y + Math.sin(a) * far }]);
1749
+ }
1750
+ }
1751
+ return out;
1752
+ }
1753
+
1754
+ // src/paint-editor/color.ts
1755
+ var HEX2 = /^#?([0-9a-f]{6})$/i;
1756
+ function parseHexColor(value, what = "color") {
1757
+ const m = typeof value === "string" ? HEX2.exec(value.trim()) : null;
1758
+ if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
1759
+ const n = parseInt(m[1], 16);
1760
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1761
+ }
1762
+ function rgbToHexColor(r, g, b) {
1763
+ return "#" + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("");
1764
+ }
1765
+ function rgbToHsv(r, g, b) {
1766
+ const R = r / 255;
1767
+ const G = g / 255;
1768
+ const B = b / 255;
1769
+ const max = Math.max(R, G, B);
1770
+ const min = Math.min(R, G, B);
1771
+ const d = max - min;
1772
+ let h = 0;
1773
+ if (d > 0) {
1774
+ if (max === R) h = 60 * ((G - B) / d % 6);
1775
+ else if (max === G) h = 60 * ((B - R) / d + 2);
1776
+ else h = 60 * ((R - G) / d + 4);
1777
+ }
1778
+ if (h < 0) h += 360;
1779
+ return { h, s: max === 0 ? 0 : d / max, v: max };
1780
+ }
1781
+ function hsvToRgb(h, s, v) {
1782
+ const hh = (h % 360 + 360) % 360 / 60;
1783
+ const c = v * s;
1784
+ const x = c * (1 - Math.abs(hh % 2 - 1));
1785
+ const m = v - c;
1786
+ 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];
1787
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
1788
+ }
1789
+ function rgbToHsl(r, g, b) {
1790
+ const { h } = rgbToHsv(r, g, b);
1791
+ const max = Math.max(r, g, b) / 255;
1792
+ const min = Math.min(r, g, b) / 255;
1793
+ const l = (max + min) / 2;
1794
+ const d = max - min;
1795
+ return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };
1796
+ }
1797
+ function hslToRgb(h, s, l) {
1798
+ const c = (1 - Math.abs(2 * l - 1)) * s;
1799
+ const v = l + c / 2;
1800
+ return hsvToRgb(h, v === 0 ? 0 : c / v, v);
1801
+ }
1802
+ var RYB_STOPS = [
1803
+ [0, 0],
1804
+ [60, 30],
1805
+ [120, 60],
1806
+ [180, 120],
1807
+ [240, 240],
1808
+ [300, 280],
1809
+ [360, 360]
1810
+ ];
1811
+ function piecewise(v, from) {
1812
+ const x = (v % 360 + 360) % 360;
1813
+ const to = from === 0 ? 1 : 0;
1814
+ for (let i = 1; i < RYB_STOPS.length; i++) {
1815
+ const a = RYB_STOPS[i - 1];
1816
+ const b = RYB_STOPS[i];
1817
+ if (x <= b[from]) return a[to] + (x - a[from]) / (b[from] - a[from]) * (b[to] - a[to]);
1818
+ }
1819
+ return x;
1820
+ }
1821
+ function wheelToHue(angle, wheel) {
1822
+ return wheel === "rgb" ? (angle % 360 + 360) % 360 : piecewise(angle, 0);
1823
+ }
1824
+ function hueToWheel(hue, wheel) {
1825
+ return wheel === "rgb" ? (hue % 360 + 360) % 360 : piecewise(hue, 1);
1826
+ }
1827
+ var HARMONY_KINDS = ["complementary", "split-complementary", "analogous", "triadic", "tetradic", "square", "monochromatic"];
1828
+ function colorHarmony(base, kind, options = {}) {
1829
+ const wheel = options.wheel ?? "ryb";
1830
+ const [r, g, b] = parseHexColor(base, "base");
1831
+ const hsv = rgbToHsv(r, g, b);
1832
+ const start = hueToWheel(hsv.h, wheel);
1833
+ const at = (angle) => {
1834
+ const a = (angle % 360 + 360) % 360;
1835
+ const [rr, gg, bb] = hsvToRgb(wheelToHue(a, wheel), hsv.s, hsv.v);
1836
+ return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(a * 10) / 10 };
1837
+ };
1838
+ const spread = options.spread ?? 30;
1839
+ const offsets = {
1840
+ complementary: [0, 180],
1841
+ "split-complementary": [0, 180 - spread, 180 + spread],
1842
+ analogous: [0, -spread, spread],
1843
+ triadic: [0, 120, 240],
1844
+ tetradic: [0, 60, 180, 240],
1845
+ square: [0, 90, 180, 270]
1846
+ };
1847
+ if (kind === "monochromatic") {
1848
+ const hsl = rgbToHsl(r, g, b);
1849
+ const steps = [0, -0.3, -0.15, 0.15, 0.3];
1850
+ return steps.map((d) => {
1851
+ 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)));
1852
+ return { color: rgbToHexColor(rr, gg, bb), angle: Math.round(start * 10) / 10 };
1853
+ });
1854
+ }
1855
+ return offsets[kind].map((o, i) => i === 0 ? { color: rgbToHexColor(r, g, b), angle: Math.round(start * 10) / 10 } : at(start + o));
1856
+ }
1857
+ var DEFAULT_SWATCHES = [
1858
+ { color: "#f7f5ee", name: "Warm white" },
1859
+ { color: "#f3d23a", name: "Lemon yellow" },
1860
+ { color: "#c8912f", name: "Yellow ochre" },
1861
+ { color: "#d23a2a", name: "Warm red" },
1862
+ { color: "#9e1f3d", name: "Crimson" },
1863
+ { color: "#8a4a2b", name: "Burnt earth" },
1864
+ { color: "#2f3f9e", name: "Deep blue" },
1865
+ { color: "#1d6f8f", name: "Cool blue" },
1866
+ { color: "#4f6e2c", name: "Leaf green" },
1867
+ { color: "#25221f", name: "Warm black" }
1868
+ ];
1869
+
1870
+ // src/paint-editor/mixing-pad.ts
1871
+ var MIXING_PAD_WIDTH = 360;
1872
+ var MIXING_PAD_HEIGHT = 220;
1873
+ var PAD_PAINT = normalizePaintBrush({
1874
+ id: "pad-paint",
1875
+ name: "Pad paint",
1876
+ category: "mixer",
1877
+ engine: "mixer",
1878
+ tip: { size: 28, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 0.7, pressureSize: false, pressureOpacity: false },
1879
+ mixer: { wet: 0.45, load: 1, mix: 0.4, loadEachStroke: true, cleanEachStroke: true, mixing: "pigment" }
1880
+ });
1881
+ var PAD_KNIFE = normalizePaintBrush({
1882
+ id: "pad-knife",
1883
+ name: "Palette knife",
1884
+ category: "smudge",
1885
+ engine: "mixer",
1886
+ loadsColor: false,
1887
+ tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow: 0.85, pressureSize: false, pressureOpacity: false },
1888
+ mixer: { wet: 1, load: 0, mix: 1, loadEachStroke: false, cleanEachStroke: true, mixing: "pigment" }
1889
+ });
1890
+ var MixingPad = class {
1891
+ width = MIXING_PAD_WIDTH;
1892
+ height = MIXING_PAD_HEIGHT;
1893
+ surface = emptySurface(4);
1894
+ /** Increments on every change (for redrawing). */
1895
+ version = 0;
1896
+ /** Paints (`paint`, with `color`) or smears (`knife`) through `points` in pad pixels. */
1897
+ stroke(points, options) {
1898
+ if (!points.length) throw new Error("points is empty; give at least one {x, y} in pad pixels.");
1899
+ for (const [i, p] of points.entries()) {
1900
+ if (!Number.isFinite(p.x) || !Number.isFinite(p.y) || p.x < 0 || p.y < 0 || p.x > this.width || p.y > this.height) {
1901
+ throw new Error(`points[${i}] (${p.x}, ${p.y}) is off the ${this.width}\xD7${this.height} mixing pad.`);
1902
+ }
1903
+ }
1904
+ let brush = options.tool === "knife" ? PAD_KNIFE : PAD_PAINT;
1905
+ if (options.size !== void 0) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);
1906
+ const [r, g, b] = hexToRgb8(options.color);
1907
+ let result = null;
1908
+ const session = new PaintStrokeSession(
1909
+ { update: () => {
1910
+ }, commit: (_s, s) => result = s },
1911
+ {
1912
+ layerId: "mixing-pad",
1913
+ surface: this.surface,
1914
+ offset: { x: 0, y: 0 },
1915
+ lockTransparency: false,
1916
+ brush,
1917
+ color: [r, g, b, 255],
1918
+ background: [255, 255, 255, 255],
1919
+ grain: null,
1920
+ paperDepth: 0,
1921
+ seed: 1,
1922
+ colorHex: options.color,
1923
+ affineCopies: [],
1924
+ copies: [IDENTITY_COPY],
1925
+ mixerLoad: mixerLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
1926
+ }
1927
+ );
1928
+ session.push(points);
1929
+ const res = session.end();
1930
+ if (result) {
1931
+ this.surface = result;
1932
+ this.version++;
1933
+ }
1934
+ return { bounds: res.bounds };
1935
+ }
1936
+ /** The average colour within `radius` pixels of (x, y), or `null` where the pad is bare. */
1937
+ pick(x, y, radius = 3) {
1938
+ 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.`);
1939
+ const r = Math.max(0, Math.round(radius));
1940
+ const x0 = Math.max(0, Math.floor(x) - r);
1941
+ const y0 = Math.max(0, Math.floor(y) - r);
1942
+ const x1 = Math.min(this.width, Math.floor(x) + r + 1);
1943
+ const y1 = Math.min(this.height, Math.floor(y) + r + 1);
1944
+ const px = readRegion(this.surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
1945
+ let R = 0;
1946
+ let G = 0;
1947
+ let B = 0;
1948
+ let A = 0;
1949
+ for (let i = 0; i < px.length; i += 4) {
1950
+ const a = px[i + 3];
1951
+ R += px[i] * a;
1952
+ G += px[i + 1] * a;
1953
+ B += px[i + 2] * a;
1954
+ A += a;
1955
+ }
1956
+ if (A === 0) return null;
1957
+ const hex = (v) => Math.round(v / A).toString(16).padStart(2, "0");
1958
+ return `#${hex(R)}${hex(G)}${hex(B)}`;
1959
+ }
1960
+ clear() {
1961
+ this.surface = emptySurface(4);
1962
+ this.version++;
1963
+ }
1964
+ /** The pad over white, `width × height` RGBA. */
1965
+ pixels() {
1966
+ const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
1967
+ for (let i = 0; i < px.length; i += 4) {
1968
+ const a = px[i + 3] / 255;
1969
+ px[i] = Math.round(px[i] * a + 255 * (1 - a));
1970
+ px[i + 1] = Math.round(px[i + 1] * a + 255 * (1 - a));
1971
+ px[i + 2] = Math.round(px[i + 2] * a + 255 * (1 - a));
1972
+ px[i + 3] = 255;
1973
+ }
1974
+ return px;
1975
+ }
1976
+ };
1977
+
1565
1978
  // src/paint-editor/controller.ts
1566
1979
  var PaintEditorError = class extends Error {
1567
1980
  constructor(message) {
@@ -1569,9 +1982,9 @@ var PaintEditorError = class extends Error {
1569
1982
  this.name = "PaintEditorError";
1570
1983
  }
1571
1984
  };
1572
- var HEX2 = /^#[0-9a-f]{6}$/i;
1985
+ var HEX3 = /^#[0-9a-f]{6}$/i;
1573
1986
  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".`);
1987
+ if (typeof value !== "string" || !HEX3.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
1575
1988
  return value.trim().toLowerCase();
1576
1989
  }
1577
1990
  function rgbaOf(hex) {
@@ -1612,6 +2025,13 @@ var PaintEditorController = class {
1612
2025
  dirty = "all";
1613
2026
  seedCounter = 1;
1614
2027
  strokeCounter = 1;
2028
+ guides;
2029
+ swatches = DEFAULT_SWATCHES.map((s) => ({ ...s }));
2030
+ pad = new MixingPad();
2031
+ tracingVersion = 1;
2032
+ tracingCache = null;
2033
+ imagePixels = /* @__PURE__ */ new Map();
2034
+ nextReferenceId = 1;
1615
2035
  viewCompositor = createPaintCompositor();
1616
2036
  exportCompositor = createPaintCompositor();
1617
2037
  constructor(options = {}) {
@@ -1623,6 +2043,7 @@ var PaintEditorController = class {
1623
2043
  this.color = options.color ? parseColor(options.color) : "#1f2937";
1624
2044
  this.hand = options.hand === "left" ? "left" : "right";
1625
2045
  this.encodeImage = options.encodeImage;
2046
+ this.guides = defaultGuides(this.doc.canvas);
1626
2047
  }
1627
2048
  // --- State and events ----------------------------------------------------------
1628
2049
  subscribe(listener) {
@@ -1654,6 +2075,9 @@ var PaintEditorController = class {
1654
2075
  canRedo: this.redoStack.length > 0 && !this.session,
1655
2076
  undoLabel: last?.label ?? null,
1656
2077
  redoLabel: next?.label ?? null,
2078
+ guides: this.guides,
2079
+ swatches: this.swatches,
2080
+ mixingPadVersion: this.pad.version,
1657
2081
  stroking: Boolean(this.session),
1658
2082
  version: this.version
1659
2083
  };
@@ -1723,6 +2147,8 @@ var PaintEditorController = class {
1723
2147
  paper: options.paper === void 0 ? this.doc.paper : this.resolvePaper(options.paper)
1724
2148
  });
1725
2149
  this.extensions = {};
2150
+ this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
2151
+ this.tracingVersion++;
1726
2152
  this.commit("New canvas", next);
1727
2153
  return next;
1728
2154
  }
@@ -2145,7 +2571,7 @@ var PaintEditorController = class {
2145
2571
  }
2146
2572
  /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
2147
2573
  strokeCopies() {
2148
- return [];
2574
+ return symmetryCopies(this.guides.symmetry);
2149
2575
  }
2150
2576
  sessionFor(doc, init, host) {
2151
2577
  const layer = (() => {
@@ -2160,7 +2586,11 @@ var PaintEditorController = class {
2160
2586
  return l;
2161
2587
  })();
2162
2588
  const brush = this.strokeBrush(init);
2163
- const color = init.color === void 0 ? this.color : parseColor(init.color);
2589
+ let color = init.color === void 0 ? this.color : parseColor(init.color);
2590
+ const tracing = this.guides.tracing;
2591
+ if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
2592
+ const perspective = this.guides.perspective;
2593
+ const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;
2164
2594
  const rgba = rgbaOf(color);
2165
2595
  const seed = init.seed ?? mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647;
2166
2596
  const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
@@ -2178,6 +2608,7 @@ var PaintEditorController = class {
2178
2608
  colorHex: color,
2179
2609
  affineCopies: copies,
2180
2610
  copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
2611
+ inputFilter: snapper ? (p) => snapper.snap(p) : void 0,
2181
2612
  mixerLoad: brush.engine === "mixer" ? mixerLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]) : EMPTY_MIXER_LOAD
2182
2613
  });
2183
2614
  return session;
@@ -2222,7 +2653,7 @@ var PaintEditorController = class {
2222
2653
  this.commit(`${brush.name} stroke`, { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke] });
2223
2654
  }
2224
2655
  };
2225
- const session = this.sessionFor(this.doc, init, host);
2656
+ const session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);
2226
2657
  this.session = session;
2227
2658
  this.emit([]);
2228
2659
  return session;
@@ -2254,7 +2685,7 @@ var PaintEditorController = class {
2254
2685
  paintStroke(init) {
2255
2686
  this.requireIdle("painting");
2256
2687
  const points = this.checkPoints(init.points);
2257
- const { doc, result, brush } = this.runStroke(this.doc, init, points);
2688
+ const { doc, result, brush } = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);
2258
2689
  if (result.mixerLoad && brush.loadsColor) this.mixerLoad = result.mixerLoad;
2259
2690
  if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);
2260
2691
  else this.emit([]);
@@ -2339,7 +2770,20 @@ var PaintEditorController = class {
2339
2770
  }
2340
2771
  /** View-only layers between the paper and the paint (the painting guides add the tracing image). */
2341
2772
  underlayLayers() {
2342
- return [];
2773
+ const t = this.guides.tracing;
2774
+ if (!t || !t.visible) return [];
2775
+ return [this.tracingNode(t)];
2776
+ }
2777
+ /** The tracing image resampled onto the canvas (cached until it or the canvas changes). */
2778
+ tracingNode(t) {
2779
+ const { width, height } = this.doc.canvas;
2780
+ const key = this.tracingVersion * 100003 + width * 7 + height;
2781
+ if (this.tracingCache?.key === key && this.tracingCache.node.opacity === t.opacity) return this.tracingCache.node;
2782
+ const src = this.imagePixels.get(t.src) ?? this.decodeImage(t.src);
2783
+ const image = resampleInto(src, t.rect, width, height);
2784
+ 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 };
2785
+ this.tracingCache = { key, node };
2786
+ return node;
2343
2787
  }
2344
2788
  renderLayers(options, view = false) {
2345
2789
  let layers = this.currentLayers();
@@ -2380,14 +2824,18 @@ var PaintEditorController = class {
2380
2824
  // --- Save, load and export -------------------------------------------------------
2381
2825
  /** The document as JSON with inline PNG assets. */
2382
2826
  toJSON(options = {}) {
2383
- return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions });
2827
+ return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
2384
2828
  }
2385
2829
  /** The document as JSON with each PNG stored in `store`. */
2386
2830
  toJSONExternal(store, options = {}) {
2387
- return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions });
2831
+ return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
2388
2832
  }
2389
2833
  adopt(loaded) {
2390
2834
  this.extensions = loaded.extensions;
2835
+ this.readGuides(loaded.guides, loaded.state, loaded.problems);
2836
+ if (Array.isArray(loaded.swatches)) {
2837
+ 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() });
2838
+ }
2391
2839
  for (const s of loaded.state.strokes) {
2392
2840
  const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
2393
2841
  if (n >= this.strokeCounter) this.strokeCounter = n + 1;
@@ -2476,20 +2924,630 @@ var PaintEditorController = class {
2476
2924
  const img = this.renderPreview(options);
2477
2925
  return { data: bytesToBase64(encodePng(img.width, img.height, img.data)), width: img.width, height: img.height };
2478
2926
  }
2927
+ // --- Guides --------------------------------------------------------------------
2928
+ setGuides(next) {
2929
+ this.guides = next;
2930
+ this.emit("all");
2931
+ return next;
2932
+ }
2933
+ /** Mirror and kaleidoscope painting: every new stroke paints its copies too (one stroke, one undo step). */
2934
+ setSymmetry(update) {
2935
+ const symmetry = normalizeSymmetry(update, this.guides.symmetry);
2936
+ this.setGuides({ ...this.guides, symmetry });
2937
+ return symmetry;
2938
+ }
2939
+ /** Perspective guides with one to three vanishing points that pointer strokes snap to. */
2940
+ setPerspective(update) {
2941
+ let perspective;
2942
+ try {
2943
+ perspective = normalizePerspective(update, this.guides.perspective);
2944
+ } catch (e) {
2945
+ throw new PaintEditorError(e.message);
2946
+ }
2947
+ this.setGuides({ ...this.guides, perspective });
2948
+ return perspective;
2949
+ }
2950
+ /** An image from a PNG (base64 or data URL) or from RGBA pixels, stored as a PNG data URL. */
2951
+ importImage(input, what) {
2952
+ let pixels;
2953
+ if (input.pixels) {
2954
+ const { width, height, data } = input.pixels;
2955
+ if (!(width > 0 && height > 0 && Number.isInteger(width) && Number.isInteger(height)) || data.length !== width * height * 4) {
2956
+ throw new PaintEditorError(`${what}: pixels must be width \xD7 height \xD7 4 bytes of RGBA.`);
2957
+ }
2958
+ pixels = { width, height, data: new Uint8ClampedArray(data) };
2959
+ } else if (typeof input.png === "string" && input.png.trim()) {
2960
+ pixels = this.decodeImage(input.png.trim().startsWith("data:") ? input.png.trim() : `data:image/png;base64,${input.png.trim()}`, what);
2961
+ } else throw new PaintEditorError(`${what}: pass png (a PNG as base64 or a data URL) or pixels.`);
2962
+ 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.`);
2963
+ const src = pngDataUrl(pixels.width, pixels.height, pixels.data);
2964
+ this.imagePixels.set(src, pixels);
2965
+ return { src, pixels };
2966
+ }
2967
+ decodeImage(src, what = "Image") {
2968
+ const cached = this.imagePixels.get(src);
2969
+ if (cached) return cached;
2970
+ const parsed = parseDataUrl(src);
2971
+ if (!parsed) throw new PaintEditorError(`${what} is not a data URL or base64 PNG.`);
2972
+ try {
2973
+ const png = decodePng(parsed.bytes);
2974
+ const px = { width: png.width, height: png.height, data: png.data };
2975
+ this.imagePixels.set(src, px);
2976
+ return px;
2977
+ } catch (e) {
2978
+ throw new PaintEditorError(`${what} could not be decoded as a PNG (${e.message}); decode other formats in the browser and pass pixels.`);
2979
+ }
2980
+ }
2981
+ /**
2982
+ * Puts an image under the paint to trace over: view only, never in exports. Placed to fit the
2983
+ * canvas unless `rect` is given; `sampleColors` makes strokes take their colour from it.
2984
+ */
2985
+ setTracingImage(options) {
2986
+ const { src, pixels } = this.importImage(options, "The tracing image");
2987
+ const { width, height } = this.doc.canvas;
2988
+ const k = Math.min(width / pixels.width, height / pixels.height);
2989
+ const fit = { x: (width - pixels.width * k) / 2, y: (height - pixels.height * k) / 2, width: pixels.width * k, height: pixels.height * k };
2990
+ const tracing = { src, width: pixels.width, height: pixels.height, rect: fit, opacity: 0.35, visible: true, sampleColors: false };
2991
+ return this.updateTracing({ ...options, rect: options.rect ?? fit }, tracing);
2992
+ }
2993
+ /** Changes the tracing image's opacity, visibility, placement or colour sampling. */
2994
+ updateTracing(update, base = this.guides.tracing) {
2995
+ if (!base) throw new PaintEditorError("There is no tracing image; set one with set_tracing_image first.");
2996
+ if (update.opacity !== void 0 && !(update.opacity >= 0 && update.opacity <= 1)) throw new PaintEditorError(`opacity ${update.opacity} is out of range; use 0..1.`);
2997
+ if (update.rect && !(update.rect.width > 0 && update.rect.height > 0)) throw new PaintEditorError("rect needs a width and height above 0.");
2998
+ const tracing = {
2999
+ ...base,
3000
+ ...update.opacity !== void 0 ? { opacity: update.opacity } : {},
3001
+ ...update.visible !== void 0 ? { visible: update.visible } : {},
3002
+ ...update.sampleColors !== void 0 ? { sampleColors: update.sampleColors } : {},
3003
+ ...update.rect ? { rect: { ...update.rect } } : {}
3004
+ };
3005
+ if (tracing.src !== base.src || update.rect) this.tracingVersion++;
3006
+ if (base !== this.guides.tracing) this.tracingVersion++;
3007
+ this.setGuides({ ...this.guides, tracing });
3008
+ return tracing;
3009
+ }
3010
+ clearTracingImage() {
3011
+ this.tracingVersion++;
3012
+ this.setGuides({ ...this.guides, tracing: null });
3013
+ }
3014
+ /** The tracing image's colour at a document pixel, or `null` outside it or where it is clear. */
3015
+ sampleTracingColor(x, y) {
3016
+ const t = this.guides.tracing;
3017
+ if (!t) return null;
3018
+ const src = this.decodeImage(t.src);
3019
+ const u = Math.floor((x - t.rect.x) / t.rect.width * src.width);
3020
+ const v = Math.floor((y - t.rect.y) / t.rect.height * src.height);
3021
+ if (u < 0 || v < 0 || u >= src.width || v >= src.height) return null;
3022
+ const i = (v * src.width + u) * 4;
3023
+ if (src.data[i + 3] === 0) return null;
3024
+ return rgbToHex(src.data[i], src.data[i + 1], src.data[i + 2]);
3025
+ }
3026
+ /** Adds a floating reference image to look at and pick colours from. */
3027
+ addReferenceImage(options) {
3028
+ const { src, pixels } = this.importImage(options, "The reference image");
3029
+ const ref = {
3030
+ id: `ref${this.nextReferenceId++}`,
3031
+ name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,
3032
+ src,
3033
+ width: pixels.width,
3034
+ height: pixels.height,
3035
+ x: options.x ?? 16 + this.guides.references.length * 24,
3036
+ y: options.y ?? 16 + this.guides.references.length * 24,
3037
+ displayWidth: options.displayWidth ?? Math.min(240, pixels.width),
3038
+ visible: true
3039
+ };
3040
+ this.setGuides({ ...this.guides, references: [...this.guides.references, ref] });
3041
+ return ref;
3042
+ }
3043
+ requireReference(id) {
3044
+ const r = this.guides.references.find((x) => x.id === id);
3045
+ if (!r) throw new PaintEditorError(`Unknown reference image "${id}". Call list_reference_images for the ids.`);
3046
+ return r;
3047
+ }
3048
+ /** Moves, resizes, renames, shows or hides a reference image. */
3049
+ updateReferenceImage(id, update) {
3050
+ const r = this.requireReference(id);
3051
+ if (update.displayWidth !== void 0 && !(update.displayWidth >= 16 && update.displayWidth <= 4096)) throw new PaintEditorError(`displayWidth ${update.displayWidth} is out of range; use 16..4096.`);
3052
+ const next = { ...r, ...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== void 0)) };
3053
+ this.setGuides({ ...this.guides, references: this.guides.references.map((x) => x.id === id ? next : x) });
3054
+ return next;
3055
+ }
3056
+ removeReferenceImage(id) {
3057
+ this.requireReference(id);
3058
+ this.setGuides({ ...this.guides, references: this.guides.references.filter((x) => x.id !== id) });
3059
+ }
3060
+ /** A reference image's pixels (decoded once). */
3061
+ referencePixels(id) {
3062
+ return this.decodeImage(this.requireReference(id).src);
3063
+ }
3064
+ /** The colour of a reference image at its pixel (x, y). */
3065
+ pickReferenceColor(id, x, y) {
3066
+ const r = this.requireReference(id);
3067
+ const px = this.decodeImage(r.src);
3068
+ const u = Math.floor(x);
3069
+ const v = Math.floor(y);
3070
+ 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.`);
3071
+ const i = (v * px.width + u) * 4;
3072
+ return rgbToHex(px.data[i], px.data[i + 1], px.data[i + 2]);
3073
+ }
3074
+ readGuides(raw, state, problems) {
3075
+ const base = defaultGuides(state.canvas);
3076
+ this.guides = base;
3077
+ this.tracingVersion++;
3078
+ if (!raw || typeof raw !== "object") return;
3079
+ const r = raw;
3080
+ const guides = { ...base };
3081
+ guides.symmetry = normalizeSymmetry(r.symmetry ?? {}, base.symmetry);
3082
+ try {
3083
+ guides.perspective = normalizePerspective(r.perspective ?? {}, base.perspective);
3084
+ } catch (e) {
3085
+ problems.push(`Perspective guide: ${e.message}`);
3086
+ }
3087
+ const t = r.tracing;
3088
+ if (t && typeof t.src === "string") {
3089
+ try {
3090
+ const px = this.decodeImage(t.src, "The tracing image");
3091
+ guides.tracing = {
3092
+ src: t.src,
3093
+ width: px.width,
3094
+ height: px.height,
3095
+ 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 },
3096
+ opacity: typeof t.opacity === "number" ? Math.min(1, Math.max(0, t.opacity)) : 0.35,
3097
+ visible: t.visible !== false,
3098
+ sampleColors: t.sampleColors === true
3099
+ };
3100
+ } catch (e) {
3101
+ problems.push(e.message);
3102
+ }
3103
+ }
3104
+ if (Array.isArray(r.references)) {
3105
+ for (const ref of r.references) {
3106
+ if (!ref || typeof ref.src !== "string" || typeof ref.id !== "string") continue;
3107
+ try {
3108
+ const px = this.decodeImage(ref.src, `Reference "${ref.name}"`);
3109
+ 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 });
3110
+ const n = Number(/^ref(\d+)$/.exec(ref.id)?.[1] ?? 0);
3111
+ if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;
3112
+ } catch (e) {
3113
+ problems.push(e.message);
3114
+ }
3115
+ }
3116
+ }
3117
+ this.guides = guides;
3118
+ }
3119
+ // --- Swatches and harmonies ------------------------------------------------------
3120
+ /** Colour harmonies around a colour (default the paint colour) on the painter's RYB wheel or the RGB wheel. */
3121
+ harmony(kind, options = {}) {
3122
+ try {
3123
+ return colorHarmony(options.color === void 0 ? this.color : parseColor(options.color), kind, options);
3124
+ } catch (e) {
3125
+ throw new PaintEditorError(e.message);
3126
+ }
3127
+ }
3128
+ addSwatch(color, name) {
3129
+ const c = parseColor(color);
3130
+ this.swatches = [...this.swatches, name?.trim() ? { color: c, name: name.trim() } : { color: c }];
3131
+ this.emit([]);
3132
+ return this.swatches;
3133
+ }
3134
+ removeSwatch(index) {
3135
+ 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).`);
3136
+ this.swatches = this.swatches.filter((_, i) => i !== index);
3137
+ this.emit([]);
3138
+ return this.swatches;
3139
+ }
3140
+ setSwatches(swatches) {
3141
+ this.swatches = swatches.map((x) => x.name ? { color: parseColor(x.color), name: x.name } : { color: parseColor(x.color) });
3142
+ this.emit([]);
3143
+ return this.swatches;
3144
+ }
3145
+ // --- Mixing pad ------------------------------------------------------------------
3146
+ get mixingPad() {
3147
+ return this.pad;
3148
+ }
3149
+ /** Paints (`paint`) or smears with the palette knife (`knife`) on the mixing pad, in pad pixels. */
3150
+ mixingPadStroke(points, options = {}) {
3151
+ const color = options.color === void 0 ? this.color : parseColor(options.color);
3152
+ 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.`);
3153
+ try {
3154
+ const r = this.pad.stroke(points, { tool: options.tool ?? "paint", color, size: options.size });
3155
+ this.emit([]);
3156
+ return r;
3157
+ } catch (e) {
3158
+ throw new PaintEditorError(e.message);
3159
+ }
3160
+ }
3161
+ /** Picks the mixed colour on the pad (averaged over `radius` px); with `setColor` it becomes the paint colour. */
3162
+ mixingPadPick(x, y, options = {}) {
3163
+ let c;
3164
+ try {
3165
+ c = this.pad.pick(x, y, options.radius);
3166
+ } catch (e) {
3167
+ throw new PaintEditorError(e.message);
3168
+ }
3169
+ if (c && options.setColor) this.setColor(c);
3170
+ return c;
3171
+ }
3172
+ clearMixingPad() {
3173
+ this.pad.clear();
3174
+ this.emit([]);
3175
+ }
2479
3176
  /** A layer's tile surface (live while stroking), for thumbnails. */
2480
3177
  layerSurface(id) {
2481
3178
  const l = findLayer(this.currentLayers(), id)?.layer;
2482
3179
  return isPixelLayer(l) ? l.surface : null;
2483
3180
  }
2484
3181
  };
3182
+ var MAX_IMAGE_SIDE = 8192;
3183
+ function resampleInto(src, rect, width, height) {
3184
+ const out = new Uint8ClampedArray(width * height * 4);
3185
+ const x0 = Math.max(0, Math.floor(rect.x));
3186
+ const y0 = Math.max(0, Math.floor(rect.y));
3187
+ const x1 = Math.min(width, Math.ceil(rect.x + rect.width));
3188
+ const y1 = Math.min(height, Math.ceil(rect.y + rect.height));
3189
+ const sx = src.width / rect.width;
3190
+ const sy = src.height / rect.height;
3191
+ const d = src.data;
3192
+ for (let y = y0; y < y1; y++) {
3193
+ const v = Math.min(src.height - 1, Math.max(0, (y + 0.5 - rect.y) * sy - 0.5));
3194
+ const j0 = Math.floor(v);
3195
+ const j1 = Math.min(src.height - 1, j0 + 1);
3196
+ const fy = v - j0;
3197
+ for (let x = x0; x < x1; x++) {
3198
+ const u = (x + 0.5 - rect.x) * sx - 0.5;
3199
+ 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;
3200
+ const uc = Math.min(src.width - 1, Math.max(0, u));
3201
+ const i0 = Math.floor(uc);
3202
+ const i1 = Math.min(src.width - 1, i0 + 1);
3203
+ const fx = uc - i0;
3204
+ const o = (y * width + x) * 4;
3205
+ for (let c = 0; c < 4; c++) {
3206
+ const a = d[(j0 * src.width + i0) * 4 + c] + (d[(j0 * src.width + i1) * 4 + c] - d[(j0 * src.width + i0) * 4 + c]) * fx;
3207
+ const b = d[(j1 * src.width + i0) * 4 + c] + (d[(j1 * src.width + i1) * 4 + c] - d[(j1 * src.width + i0) * 4 + c]) * fx;
3208
+ out[o + c] = a + (b - a) * fy;
3209
+ }
3210
+ }
3211
+ }
3212
+ return { width, height, data: out };
3213
+ }
2485
3214
 
2486
- // src/paint-editor/tools.ts
3215
+ // src/paint-editor/guide-tools.ts
2487
3216
  var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
2488
3217
  var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
2489
3218
  var SET = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
2490
3219
  var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
2491
3220
  var EMPTY = { type: "object", properties: {}, additionalProperties: false };
2492
- var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
3221
+ var COLOR = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Colour #rrggbb." };
3222
+ var POINT = {
3223
+ type: "object",
3224
+ properties: { x: { type: "number", description: "x in document pixels." }, y: { type: "number", description: "y in document pixels." } },
3225
+ required: ["x", "y"],
3226
+ additionalProperties: false
3227
+ };
3228
+ var RECT = {
3229
+ type: "object",
3230
+ description: "Placement in document pixels.",
3231
+ properties: {
3232
+ x: { type: "number", description: "Left edge." },
3233
+ y: { type: "number", description: "Top edge." },
3234
+ width: { type: "number", exclusiveMinimum: 0, description: "Width." },
3235
+ height: { type: "number", exclusiveMinimum: 0, description: "Height." }
3236
+ },
3237
+ required: ["x", "y", "width", "height"],
3238
+ additionalProperties: false
3239
+ };
3240
+ var PNG = { type: "string", minLength: 1, description: "The image as a PNG, base64 or a data:image/png URL." };
3241
+ var tracingInfo = (t) => t ? { width: t.width, height: t.height, rect: t.rect, opacity: t.opacity, visible: t.visible, sampleColors: t.sampleColors } : null;
3242
+ var refInfo = ({ src: _s, ...r }) => r;
3243
+ function createPaintGuideTools(c) {
3244
+ const guides = () => c.getState().guides;
3245
+ return [
3246
+ {
3247
+ name: "get_guides",
3248
+ title: "Get the painting guides",
3249
+ 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.",
3250
+ inputSchema: EMPTY,
3251
+ annotations: { title: "Get the painting guides", ...READ },
3252
+ handler: () => {
3253
+ const g = guides();
3254
+ return { symmetry: { ...g.symmetry, copies: symmetryCopies(g.symmetry).length }, perspective: g.perspective, tracing: tracingInfo(g.tracing), references: g.references.map(refInfo) };
3255
+ }
3256
+ },
3257
+ {
3258
+ name: "set_symmetry",
3259
+ title: "Set mirror or kaleidoscope painting",
3260
+ 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.",
3261
+ inputSchema: {
3262
+ type: "object",
3263
+ properties: {
3264
+ mode: { type: "string", enum: [...SYMMETRY_MODES], description: "off, vertical (mirror left / right), horizontal (top / bottom), dual, diagonal or kaleidoscope." },
3265
+ center: { ...POINT, description: "Centre of the axes in document pixels (default the canvas centre)." },
3266
+ rotation: { type: "number", minimum: -360, maximum: 360, description: "Turn the axes, degrees clockwise." },
3267
+ segments: { type: "integer", minimum: 2, maximum: 32, description: "Kaleidoscope segments (default 6)." },
3268
+ mirror: { type: "boolean", description: "Kaleidoscope: also reflect alternate segments (default true)." }
3269
+ },
3270
+ additionalProperties: false
3271
+ },
3272
+ annotations: { title: "Set mirror or kaleidoscope painting", ...SET },
3273
+ handler: (a) => {
3274
+ const s = c.setSymmetry(a);
3275
+ return { ...s, copies: symmetryCopies(s).length };
3276
+ }
3277
+ },
3278
+ {
3279
+ name: "set_perspective_guide",
3280
+ title: "Set the perspective guide",
3281
+ 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.",
3282
+ inputSchema: {
3283
+ type: "object",
3284
+ properties: {
3285
+ enabled: { type: "boolean", description: "Show and use the guide." },
3286
+ points: { type: "array", minItems: 1, maxItems: 3, items: { ...POINT, description: "A vanishing point." }, description: "The vanishing points." },
3287
+ snap: { type: "boolean", description: "Snap strokes to the guide lines (default true)." }
3288
+ },
3289
+ additionalProperties: false
3290
+ },
3291
+ annotations: { title: "Set the perspective guide", ...SET },
3292
+ handler: (a) => c.setPerspective(a)
3293
+ },
3294
+ {
3295
+ name: "set_tracing_image",
3296
+ title: "Set a tracing image",
3297
+ 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.",
3298
+ inputSchema: {
3299
+ type: "object",
3300
+ properties: {
3301
+ png: PNG,
3302
+ rect: RECT,
3303
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 0.35)." },
3304
+ sampleColors: { type: "boolean", description: "Strokes take their colour from the image (default false)." }
3305
+ },
3306
+ required: ["png"],
3307
+ additionalProperties: false
3308
+ },
3309
+ annotations: { title: "Set a tracing image", ...WRITE },
3310
+ handler: (a) => tracingInfo(c.setTracingImage(a))
3311
+ },
3312
+ {
3313
+ name: "update_tracing_image",
3314
+ title: "Change the tracing image",
3315
+ description: "Changes the tracing image opacity, visibility, placement or colour sampling. Returns it.",
3316
+ inputSchema: {
3317
+ type: "object",
3318
+ properties: {
3319
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1." },
3320
+ visible: { type: "boolean", description: "Show or hide it." },
3321
+ sampleColors: { type: "boolean", description: "Strokes take their colour from it." },
3322
+ rect: RECT
3323
+ },
3324
+ additionalProperties: false
3325
+ },
3326
+ annotations: { title: "Change the tracing image", ...SET },
3327
+ handler: (a) => tracingInfo(c.updateTracing(a))
3328
+ },
3329
+ {
3330
+ name: "clear_tracing_image",
3331
+ title: "Remove the tracing image",
3332
+ description: "Removes the tracing image. Returns null.",
3333
+ inputSchema: EMPTY,
3334
+ annotations: { title: "Remove the tracing image", ...DESTRUCTIVE },
3335
+ handler: () => {
3336
+ c.clearTracingImage();
3337
+ return { tracing: null };
3338
+ }
3339
+ },
3340
+ {
3341
+ name: "add_reference_image",
3342
+ title: "Add a reference image",
3343
+ 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.",
3344
+ inputSchema: {
3345
+ type: "object",
3346
+ properties: {
3347
+ png: PNG,
3348
+ name: { type: "string", minLength: 1, description: "Display name." },
3349
+ x: { type: "number", description: "Panel left in viewport pixels." },
3350
+ y: { type: "number", description: "Panel top in viewport pixels." },
3351
+ displayWidth: { type: "number", minimum: 16, maximum: 4096, description: "Panel width in screen pixels (default up to 240)." }
3352
+ },
3353
+ required: ["png"],
3354
+ additionalProperties: false
3355
+ },
3356
+ annotations: { title: "Add a reference image", ...WRITE },
3357
+ handler: (a) => refInfo(c.addReferenceImage(a))
3358
+ },
3359
+ {
3360
+ name: "list_reference_images",
3361
+ title: "List reference images",
3362
+ description: "Lists the reference images: id, name, size in pixels, panel placement and visibility.",
3363
+ inputSchema: EMPTY,
3364
+ annotations: { title: "List reference images", ...READ },
3365
+ handler: () => ({ references: guides().references.map(refInfo) })
3366
+ },
3367
+ {
3368
+ name: "update_reference_image",
3369
+ title: "Change a reference image",
3370
+ description: "Moves, resizes, renames, shows or hides a reference image panel. Returns it.",
3371
+ inputSchema: {
3372
+ type: "object",
3373
+ properties: {
3374
+ id: { type: "string", description: "Reference id from list_reference_images." },
3375
+ name: { type: "string", minLength: 1, description: "Display name." },
3376
+ x: { type: "number", description: "Panel left in viewport pixels." },
3377
+ y: { type: "number", description: "Panel top in viewport pixels." },
3378
+ displayWidth: { type: "number", minimum: 16, maximum: 4096, description: "Panel width in screen pixels." },
3379
+ visible: { type: "boolean", description: "Show or hide it." }
3380
+ },
3381
+ required: ["id"],
3382
+ additionalProperties: false
3383
+ },
3384
+ annotations: { title: "Change a reference image", ...SET },
3385
+ handler: ({ id, ...u }) => refInfo(c.updateReferenceImage(id, u))
3386
+ },
3387
+ {
3388
+ name: "remove_reference_image",
3389
+ title: "Remove a reference image",
3390
+ description: "Removes a reference image. Returns the removed id.",
3391
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "Reference id from list_reference_images." } }, required: ["id"], additionalProperties: false },
3392
+ annotations: { title: "Remove a reference image", ...DESTRUCTIVE },
3393
+ handler: ({ id }) => {
3394
+ c.removeReferenceImage(id);
3395
+ return { removed: id };
3396
+ }
3397
+ },
3398
+ {
3399
+ name: "pick_reference_color",
3400
+ title: "Pick a colour from a reference",
3401
+ description: "Reads the colour of a reference image at its own pixel (x, y). With setColor it becomes the paint colour. Returns #rrggbb.",
3402
+ inputSchema: {
3403
+ type: "object",
3404
+ properties: {
3405
+ id: { type: "string", description: "Reference id from list_reference_images." },
3406
+ x: { type: "number", minimum: 0, description: "x in the image\u2019s pixels." },
3407
+ y: { type: "number", minimum: 0, description: "y in the image\u2019s pixels." },
3408
+ setColor: { type: "boolean", description: "Also make it the paint colour (default false)." }
3409
+ },
3410
+ required: ["id", "x", "y"],
3411
+ additionalProperties: false
3412
+ },
3413
+ annotations: { title: "Pick a colour from a reference", ...SET },
3414
+ handler: ({ id, x, y, setColor }) => {
3415
+ const color = c.pickReferenceColor(id, x, y);
3416
+ if (setColor) c.setColor(color);
3417
+ return { color };
3418
+ }
3419
+ },
3420
+ {
3421
+ name: "color_harmony",
3422
+ title: "Colour harmonies",
3423
+ 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.",
3424
+ inputSchema: {
3425
+ type: "object",
3426
+ properties: {
3427
+ kind: { type: "string", enum: [...HARMONY_KINDS], description: "The harmony." },
3428
+ color: { ...COLOR, description: "Base colour #rrggbb (default the paint colour)." },
3429
+ wheel: { type: "string", enum: ["ryb", "rgb"], description: "ryb (painter\u2019s wheel, default) or rgb." },
3430
+ spread: { type: "number", minimum: 5, maximum: 90, description: "Analogous and split-complementary spread in degrees (default 30)." }
3431
+ },
3432
+ required: ["kind"],
3433
+ additionalProperties: false
3434
+ },
3435
+ annotations: { title: "Colour harmonies", ...READ },
3436
+ handler: ({ kind, color, wheel, spread }) => ({ kind, wheel: wheel ?? "ryb", colors: c.harmony(kind, { color, wheel, spread }) })
3437
+ },
3438
+ {
3439
+ name: "list_swatches",
3440
+ title: "List swatches",
3441
+ description: "Lists the colour swatches (index, colour, name).",
3442
+ inputSchema: EMPTY,
3443
+ annotations: { title: "List swatches", ...READ },
3444
+ handler: () => ({ swatches: c.getState().swatches.map((s, index) => ({ index, ...s })) })
3445
+ },
3446
+ {
3447
+ name: "add_swatch",
3448
+ title: "Add a swatch",
3449
+ description: "Adds a colour to the swatches (saved with the document). Returns the swatches.",
3450
+ inputSchema: {
3451
+ type: "object",
3452
+ properties: { color: COLOR, name: { type: "string", minLength: 1, description: "Optional name." } },
3453
+ required: ["color"],
3454
+ additionalProperties: false
3455
+ },
3456
+ annotations: { title: "Add a swatch", ...WRITE },
3457
+ handler: ({ color, name }) => ({ swatches: c.addSwatch(color, name).map((s, index) => ({ index, ...s })) })
3458
+ },
3459
+ {
3460
+ name: "remove_swatch",
3461
+ title: "Remove a swatch",
3462
+ description: "Removes a swatch by index (from list_swatches). Returns the swatches.",
3463
+ inputSchema: { type: "object", properties: { index: { type: "integer", minimum: 0, description: "0-based index." } }, required: ["index"], additionalProperties: false },
3464
+ annotations: { title: "Remove a swatch", ...DESTRUCTIVE },
3465
+ handler: ({ index }) => ({ swatches: c.removeSwatch(index).map((s, i) => ({ index: i, ...s })) })
3466
+ },
3467
+ {
3468
+ name: "mixing_pad_stroke",
3469
+ title: "Paint on the mixing pad",
3470
+ 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.",
3471
+ inputSchema: {
3472
+ type: "object",
3473
+ properties: {
3474
+ points: {
3475
+ type: "array",
3476
+ minItems: 1,
3477
+ maxItems: 2e3,
3478
+ description: "The path in pad pixels (0..360, 0..220).",
3479
+ items: {
3480
+ type: "object",
3481
+ properties: { x: { type: "number", description: "Pad x." }, y: { type: "number", description: "Pad y." }, pressure: { type: "number", minimum: 0, maximum: 1, description: "Pressure (default 1)." } },
3482
+ required: ["x", "y"],
3483
+ additionalProperties: false
3484
+ }
3485
+ },
3486
+ tool: { type: "string", enum: ["paint", "knife"], description: "paint (default) or knife." },
3487
+ color: { ...COLOR, description: "Paint colour (default the paint colour; ignored by the knife)." },
3488
+ size: { type: "number", minimum: 1, maximum: 200, description: "Tool size in pad pixels (default 28 paint, 34 knife)." }
3489
+ },
3490
+ required: ["points"],
3491
+ additionalProperties: false
3492
+ },
3493
+ annotations: { title: "Paint on the mixing pad", ...WRITE },
3494
+ handler: ({ points, tool, color, size }) => c.mixingPadStroke(points, { tool, color, size })
3495
+ },
3496
+ {
3497
+ name: "pick_mixing_pad_color",
3498
+ title: "Pick from the mixing pad",
3499
+ 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.",
3500
+ inputSchema: {
3501
+ type: "object",
3502
+ properties: {
3503
+ x: { type: "number", minimum: 0, maximum: 360, description: "Pad x." },
3504
+ y: { type: "number", minimum: 0, maximum: 220, description: "Pad y." },
3505
+ radius: { type: "integer", minimum: 0, maximum: 20, description: "Averaging radius in pixels (default 3)." },
3506
+ setColor: { type: "boolean", description: "Also make it the paint colour (default false)." }
3507
+ },
3508
+ required: ["x", "y"],
3509
+ additionalProperties: false
3510
+ },
3511
+ annotations: { title: "Pick from the mixing pad", ...SET },
3512
+ handler: ({ x, y, radius, setColor }) => ({ color: c.mixingPadPick(x, y, { radius, setColor }) })
3513
+ },
3514
+ {
3515
+ name: "clear_mixing_pad",
3516
+ title: "Clear the mixing pad",
3517
+ description: "Wipes the mixing pad clean.",
3518
+ inputSchema: EMPTY,
3519
+ annotations: { title: "Clear the mixing pad", ...DESTRUCTIVE },
3520
+ handler: () => {
3521
+ c.clearMixingPad();
3522
+ return { cleared: true };
3523
+ }
3524
+ },
3525
+ {
3526
+ name: "render_mixing_pad",
3527
+ title: "Render the mixing pad",
3528
+ description: "Renders the mixing pad as a PNG (360 \xD7 220, over white). Changes nothing.",
3529
+ inputSchema: EMPTY,
3530
+ annotations: { title: "Render the mixing pad", ...READ },
3531
+ handler: () => {
3532
+ const pad = c.mixingPad;
3533
+ return {
3534
+ content: [
3535
+ { type: "image", data: bytesToBase64(encodePng(pad.width, pad.height, pad.pixels())), mimeType: "image/png" },
3536
+ { type: "text", text: `Mixing pad ${pad.width}\xD7${pad.height}` }
3537
+ ]
3538
+ };
3539
+ }
3540
+ }
3541
+ ];
3542
+ }
3543
+
3544
+ // src/paint-editor/tools.ts
3545
+ var READ2 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3546
+ var WRITE2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
3547
+ var SET2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3548
+ var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
3549
+ var EMPTY2 = { type: "object", properties: {}, additionalProperties: false };
3550
+ var COLOR2 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
2493
3551
  var LAYER_ID = { type: "string", description: "Layer id from list_layers (default the active layer)." };
2494
3552
  var TIP = {
2495
3553
  type: "object",
@@ -2589,8 +3647,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
2589
3647
  name: "get_canvas_info",
2590
3648
  title: "Get the canvas",
2591
3649
  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 },
3650
+ inputSchema: EMPTY2,
3651
+ annotations: { title: "Get the canvas", ...READ2 },
2594
3652
  handler: () => {
2595
3653
  const s = c.getState();
2596
3654
  const d = s.document;
@@ -2627,7 +3685,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2627
3685
  },
2628
3686
  additionalProperties: false
2629
3687
  },
2630
- annotations: { title: "Start a new canvas", ...DESTRUCTIVE },
3688
+ annotations: { title: "Start a new canvas", ...DESTRUCTIVE2 },
2631
3689
  handler: (a) => {
2632
3690
  const d = c.newCanvas({ width: a.width, height: a.height, dpi: a.dpi, paper: a.paper });
2633
3691
  return { width: d.canvas.width, height: d.canvas.height, dpi: d.canvas.dpi, paper: d.paper.preset, layerId: d.activeLayerId };
@@ -2646,15 +3704,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
2646
3704
  },
2647
3705
  additionalProperties: false
2648
3706
  },
2649
- annotations: { title: "Resize the canvas", ...SET },
3707
+ annotations: { title: "Resize the canvas", ...SET2 },
2650
3708
  handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
2651
3709
  });
2652
3710
  add({
2653
3711
  name: "list_papers",
2654
3712
  title: "List papers",
2655
3713
  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 },
3714
+ inputSchema: EMPTY2,
3715
+ annotations: { title: "List papers", ...READ2 },
2658
3716
  handler: () => ({
2659
3717
  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
3718
  families: [...PAPER_FAMILIES],
@@ -2665,8 +3723,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
2665
3723
  name: "get_paper",
2666
3724
  title: "Get the paper",
2667
3725
  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 },
3726
+ inputSchema: EMPTY2,
3727
+ annotations: { title: "Get the paper", ...READ2 },
2670
3728
  handler: () => JSON.parse(JSON.stringify(c.document.paper))
2671
3729
  });
2672
3730
  const tuning2 = {
@@ -2685,7 +3743,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2685
3743
  type: "object",
2686
3744
  properties: {
2687
3745
  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." },
3746
+ color: { ...COLOR2, description: "Paper colour #rrggbb." },
2689
3747
  depth: { type: "number", minimum: 0, maximum: 1, description: "Grain depth 0..1 (0: brushes ignore the grain)." },
2690
3748
  scale: { type: "number", minimum: 0.25, maximum: 8, description: "Grain scale: 1 natural, 2 twice as coarse." },
2691
3749
  seed: { type: "integer", minimum: 0, description: "Grain seed: another sheet of the same paper." },
@@ -2701,15 +3759,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
2701
3759
  },
2702
3760
  additionalProperties: false
2703
3761
  },
2704
- annotations: { title: "Set the paper", ...SET },
3762
+ annotations: { title: "Set the paper", ...SET2 },
2705
3763
  handler: (a) => c.setPaper(a)
2706
3764
  });
2707
3765
  add({
2708
3766
  name: "list_layers",
2709
3767
  title: "List layers",
2710
3768
  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 },
3769
+ inputSchema: EMPTY2,
3770
+ annotations: { title: "List layers", ...READ2 },
2713
3771
  handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
2714
3772
  });
2715
3773
  add({
@@ -2726,7 +3784,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2726
3784
  },
2727
3785
  additionalProperties: false
2728
3786
  },
2729
- annotations: { title: "Add a layer", ...WRITE },
3787
+ annotations: { title: "Add a layer", ...WRITE2 },
2730
3788
  handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
2731
3789
  });
2732
3790
  add({
@@ -2757,7 +3815,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2757
3815
  required: ["layerId"],
2758
3816
  additionalProperties: false
2759
3817
  },
2760
- annotations: { title: "Change a layer", ...SET },
3818
+ annotations: { title: "Change a layer", ...SET2 },
2761
3819
  handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
2762
3820
  });
2763
3821
  add({
@@ -2765,7 +3823,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2765
3823
  title: "Choose the active layer",
2766
3824
  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
3825
  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 },
3826
+ annotations: { title: "Choose the active layer", ...SET2 },
2769
3827
  handler: ({ layerId }) => {
2770
3828
  c.setActiveLayer(layerId);
2771
3829
  return c.describeLayer(c.getLayer(layerId));
@@ -2776,7 +3834,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2776
3834
  title: "Remove a layer",
2777
3835
  description: "Removes a layer (a group with its children). One undo step. Returns the removed id and the new active layer.",
2778
3836
  inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2779
- annotations: { title: "Remove a layer", ...DESTRUCTIVE },
3837
+ annotations: { title: "Remove a layer", ...DESTRUCTIVE2 },
2780
3838
  handler: ({ layerId }) => c.removeLayer(layerId)
2781
3839
  });
2782
3840
  add({
@@ -2784,7 +3842,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2784
3842
  title: "Duplicate a layer",
2785
3843
  description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
2786
3844
  inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
2787
- annotations: { title: "Duplicate a layer", ...WRITE },
3845
+ annotations: { title: "Duplicate a layer", ...WRITE2 },
2788
3846
  handler: ({ layerId }) => c.duplicateLayer(layerId)
2789
3847
  });
2790
3848
  add({
@@ -2801,7 +3859,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2801
3859
  required: ["layerId", "index"],
2802
3860
  additionalProperties: false
2803
3861
  },
2804
- annotations: { title: "Reorder a layer", ...WRITE },
3862
+ annotations: { title: "Reorder a layer", ...WRITE2 },
2805
3863
  handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
2806
3864
  });
2807
3865
  add({
@@ -2809,7 +3867,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2809
3867
  title: "Merge a layer down",
2810
3868
  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
3869
  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 },
3870
+ annotations: { title: "Merge a layer down", ...DESTRUCTIVE2 },
2813
3871
  handler: ({ layerId }) => c.mergeDown(layerId)
2814
3872
  });
2815
3873
  add({
@@ -2820,7 +3878,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2820
3878
  type: "object",
2821
3879
  properties: {
2822
3880
  layerId: LAYER_ID,
2823
- color: { ...COLOR, description: "Fill colour #rrggbb (default the paint colour)." },
3881
+ color: { ...COLOR2, description: "Fill colour #rrggbb (default the paint colour)." },
2824
3882
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
2825
3883
  grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
2826
3884
  pressure: { type: "number", minimum: 0, maximum: 1, description: "With grain: how far into the valleys, 0..1 (default 0.5)." },
@@ -2828,7 +3886,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2828
3886
  },
2829
3887
  additionalProperties: false
2830
3888
  },
2831
- annotations: { title: "Fill a layer", ...WRITE },
3889
+ annotations: { title: "Fill a layer", ...WRITE2 },
2832
3890
  handler: (a) => c.fillLayer(a)
2833
3891
  });
2834
3892
  add({
@@ -2836,15 +3894,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
2836
3894
  title: "Clear a layer",
2837
3895
  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
3896
  inputSchema: { type: "object", properties: { layerId: LAYER_ID }, additionalProperties: false },
2839
- annotations: { title: "Clear a layer", ...DESTRUCTIVE },
3897
+ annotations: { title: "Clear a layer", ...DESTRUCTIVE2 },
2840
3898
  handler: ({ layerId }) => c.clearLayer(layerId)
2841
3899
  });
2842
3900
  add({
2843
3901
  name: "list_brushes",
2844
3902
  title: "List brushes",
2845
3903
  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 },
3904
+ inputSchema: EMPTY2,
3905
+ annotations: { title: "List brushes", ...READ2 },
2848
3906
  handler: () => {
2849
3907
  const s = c.getState();
2850
3908
  return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(c.getBrush(s.brush.id)?.builtin), brushes: s.brushes.map(brushSummary) };
@@ -2855,7 +3913,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2855
3913
  title: "Get a brush",
2856
3914
  description: "Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer). Without id, the active brush as edited.",
2857
3915
  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 },
3916
+ annotations: { title: "Get a brush", ...READ2 },
2859
3917
  handler: ({ id }) => {
2860
3918
  if (id === void 0) return brushToJSON(c.getState().brush);
2861
3919
  const b = c.getBrush(id);
@@ -2868,7 +3926,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2868
3926
  title: "Set the brush",
2869
3927
  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
3928
  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 },
3929
+ annotations: { title: "Set the brush", ...SET2 },
2872
3930
  handler: (a) => brushToJSON(c.setBrush(a))
2873
3931
  });
2874
3932
  add({
@@ -2884,7 +3942,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2884
3942
  required: ["id"],
2885
3943
  additionalProperties: false
2886
3944
  },
2887
- annotations: { title: "Save a brush", ...SET },
3945
+ annotations: { title: "Save a brush", ...SET2 },
2888
3946
  handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name }))
2889
3947
  });
2890
3948
  add({
@@ -2892,7 +3950,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2892
3950
  title: "Remove a saved brush",
2893
3951
  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
3952
  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 },
3953
+ annotations: { title: "Remove a saved brush", ...DESTRUCTIVE2 },
2896
3954
  handler: ({ id }) => {
2897
3955
  c.removeBrush(id);
2898
3956
  return { removed: id };
@@ -2907,7 +3965,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2907
3965
  properties: { ids: { type: "array", items: { type: "string", description: "Brush id." }, description: "Brush ids from list_brushes (default every user brush)." } },
2908
3966
  additionalProperties: false
2909
3967
  },
2910
- annotations: { title: "Export brushes", ...READ },
3968
+ annotations: { title: "Export brushes", ...READ2 },
2911
3969
  handler: ({ ids }) => c.exportBrushes(ids)
2912
3970
  });
2913
3971
  add({
@@ -2923,7 +3981,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
2923
3981
  required: ["library"],
2924
3982
  additionalProperties: false
2925
3983
  },
2926
- annotations: { title: "Import brushes", ...WRITE },
3984
+ annotations: { title: "Import brushes", ...WRITE2 },
2927
3985
  handler: ({ library, replace }) => c.importBrushes(library, { replace })
2928
3986
  });
2929
3987
  add({
@@ -2936,11 +3994,11 @@ function createPaintEditorToolProvider(controller, options = {}) {
2936
3994
  id: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
2937
3995
  width: { type: "integer", minimum: 16, maximum: 512, description: "Width in pixels (default 160)." },
2938
3996
  height: { type: "integer", minimum: 16, maximum: 512, description: "Height in pixels (default 48)." },
2939
- color: { ...COLOR, description: "Stroke colour (default the paint colour)." }
3997
+ color: { ...COLOR2, description: "Stroke colour (default the paint colour)." }
2940
3998
  },
2941
3999
  additionalProperties: false
2942
4000
  },
2943
- annotations: { title: "Render a brush thumbnail", ...READ },
4001
+ annotations: { title: "Render a brush thumbnail", ...READ2 },
2944
4002
  handler: ({ id, width, height, color }) => {
2945
4003
  const t = c.brushThumbnail(id ?? c.getState().brush.id, { width, height, color });
2946
4004
  return {
@@ -2957,10 +4015,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
2957
4015
  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
4016
  inputSchema: {
2959
4017
  type: "object",
2960
- properties: { color: { ...COLOR, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR, description: "Second colour #rrggbb." } },
4018
+ properties: { color: { ...COLOR2, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR2, description: "Second colour #rrggbb." } },
2961
4019
  additionalProperties: false
2962
4020
  },
2963
- annotations: { title: "Set the paint colour", ...SET },
4021
+ annotations: { title: "Set the paint colour", ...SET2 },
2964
4022
  handler: ({ color, secondaryColor }) => {
2965
4023
  if (color !== void 0) c.setColor(color);
2966
4024
  if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
@@ -2982,23 +4040,23 @@ function createPaintEditorToolProvider(controller, options = {}) {
2982
4040
  required: ["x", "y"],
2983
4041
  additionalProperties: false
2984
4042
  },
2985
- annotations: { title: "Pick a colour", ...READ },
4043
+ annotations: { title: "Pick a colour", ...READ2 },
2986
4044
  handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
2987
4045
  });
2988
4046
  add({
2989
4047
  name: "get_mixer_brush",
2990
4048
  title: "Get the Mixer Brush load",
2991
4049
  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 },
4050
+ inputSchema: EMPTY2,
4051
+ annotations: { title: "Get the Mixer Brush load", ...READ2 },
2994
4052
  handler: () => loadResult(c.getState().mixerLoad)
2995
4053
  });
2996
4054
  add({
2997
4055
  name: "load_mixer_brush",
2998
4056
  title: "Load the Mixer Brush",
2999
4057
  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 },
4058
+ inputSchema: { type: "object", properties: { color: { ...COLOR2, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
4059
+ annotations: { title: "Load the Mixer Brush", ...SET2 },
3002
4060
  handler: ({ color }) => loadResult(c.loadMixerBrush(color))
3003
4061
  });
3004
4062
  add({
@@ -3006,13 +4064,13 @@ function createPaintEditorToolProvider(controller, options = {}) {
3006
4064
  title: "Clean the Mixer Brush",
3007
4065
  description: "Empties the Mixer Brush pickup well (and with emptyReservoir its paint too). Editor state, not an undo step. Returns the load.",
3008
4066
  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 },
4067
+ annotations: { title: "Clean the Mixer Brush", ...SET2 },
3010
4068
  handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
3011
4069
  });
3012
4070
  add({
3013
4071
  name: "paint_stroke",
3014
4072
  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.",
4073
+ 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
4074
  inputSchema: {
3017
4075
  type: "object",
3018
4076
  properties: {
@@ -3020,15 +4078,16 @@ function createPaintEditorToolProvider(controller, options = {}) {
3020
4078
  layerId: LAYER_ID,
3021
4079
  brushId: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
3022
4080
  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." }
4081
+ color: { ...COLOR2, description: "Paint colour #rrggbb (default the paint colour)." },
4082
+ seed: { type: "integer", minimum: 0, description: "Jitter seed (default a new one); the same seed and points give the same marks." },
4083
+ snapToPerspective: { type: "boolean", description: "Snap the points to the perspective guide, as a pen stroke would (default false)." }
3025
4084
  },
3026
4085
  required: ["points"],
3027
4086
  additionalProperties: false
3028
4087
  },
3029
- annotations: { title: "Paint a brush stroke", ...WRITE },
4088
+ annotations: { title: "Paint a brush stroke", ...WRITE2 },
3030
4089
  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 });
4090
+ 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
4091
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
3033
4092
  }
3034
4093
  });
@@ -3044,7 +4103,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3044
4103
  },
3045
4104
  additionalProperties: false
3046
4105
  },
3047
- annotations: { title: "List recorded strokes", ...READ },
4106
+ annotations: { title: "List recorded strokes", ...READ2 },
3048
4107
  handler: ({ ids, last }) => {
3049
4108
  const all = c.getStrokes();
3050
4109
  const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(last ?? 100));
@@ -3093,7 +4152,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3093
4152
  additionalProperties: false
3094
4153
  },
3095
4154
  pressureScale: { type: "number", minimum: 0, maximum: 4, description: "Multiply every pressure (default 1)." },
3096
- color: { ...COLOR, description: "Recolour (default each stroke\u2019s colour)." },
4155
+ color: { ...COLOR2, description: "Recolour (default each stroke\u2019s colour)." },
3097
4156
  brushId: { type: "string", description: "Replay with this brush from list_brushes (default each stroke\u2019s brush)." },
3098
4157
  layerId: { type: "string", description: "Paint on this layer (default each stroke\u2019s layer)." },
3099
4158
  repeat: { type: "integer", minimum: 1, maximum: 100, description: "How many copies (default 1)." }
@@ -3101,7 +4160,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3101
4160
  required: ["strokeIds"],
3102
4161
  additionalProperties: false
3103
4162
  },
3104
- annotations: { title: "Replay strokes, varied", ...WRITE },
4163
+ annotations: { title: "Replay strokes, varied", ...WRITE2 },
3105
4164
  handler: (a) => c.replayStrokes(a)
3106
4165
  });
3107
4166
  add({
@@ -3118,7 +4177,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3118
4177
  },
3119
4178
  additionalProperties: false
3120
4179
  },
3121
- annotations: { title: "Rotate, mirror or zoom the view", ...SET },
4180
+ annotations: { title: "Rotate, mirror or zoom the view", ...SET2 },
3122
4181
  handler: ({ reset, ...v }) => {
3123
4182
  if (reset) c.resetView();
3124
4183
  return c.setView(v);
@@ -3129,15 +4188,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
3129
4188
  title: "Set the drawing hand",
3130
4189
  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
4190
  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 },
4191
+ annotations: { title: "Set the drawing hand", ...SET2 },
3133
4192
  handler: ({ hand }) => ({ hand: c.setHand(hand) })
3134
4193
  });
3135
4194
  add({
3136
4195
  name: "undo_paint",
3137
4196
  title: "Undo",
3138
4197
  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 },
4198
+ inputSchema: EMPTY2,
4199
+ annotations: { title: "Undo", ...WRITE2 },
3141
4200
  handler: () => {
3142
4201
  const undone = c.undo();
3143
4202
  const s = c.getState();
@@ -3148,8 +4207,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
3148
4207
  name: "redo_paint",
3149
4208
  title: "Redo",
3150
4209
  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 },
4210
+ inputSchema: EMPTY2,
4211
+ annotations: { title: "Redo", ...WRITE2 },
3153
4212
  handler: () => {
3154
4213
  const redone = c.redo();
3155
4214
  const s = c.getState();
@@ -3169,7 +4228,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3169
4228
  },
3170
4229
  additionalProperties: false
3171
4230
  },
3172
- annotations: { title: "Render a preview", ...READ },
4231
+ annotations: { title: "Render a preview", ...READ2 },
3173
4232
  handler: ({ maxSide, layerIds, paper }) => {
3174
4233
  if (layerIds) {
3175
4234
  for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
@@ -3192,7 +4251,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3192
4251
  },
3193
4252
  additionalProperties: false
3194
4253
  },
3195
- annotations: { title: "Export the painting", ...READ },
4254
+ annotations: { title: "Export the painting", ...READ2 },
3196
4255
  handler: async ({ format, quality, scale, paper }) => {
3197
4256
  const bytes = await c.exportImage({ format, quality, scale, paper });
3198
4257
  return { mimeType: format ?? "image/png", bytes: bytes.length, data: bytesToBase64(bytes) };
@@ -3202,8 +4261,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
3202
4261
  name: "export_psd",
3203
4262
  title: "Export as PSD",
3204
4263
  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 },
4264
+ inputSchema: EMPTY2,
4265
+ annotations: { title: "Export as PSD", ...READ2 },
3207
4266
  handler: () => {
3208
4267
  const r = c.exportPsd();
3209
4268
  return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
@@ -3214,7 +4273,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
3214
4273
  title: "Save the document",
3215
4274
  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
4275
  inputSchema: { type: "object", properties: { strokes: { type: "boolean", description: "Include the recorded strokes (default true)." } }, additionalProperties: false },
3217
- annotations: { title: "Save the document", ...READ },
4276
+ annotations: { title: "Save the document", ...READ2 },
3218
4277
  handler: ({ strokes }) => c.toJSON({ strokes })
3219
4278
  });
3220
4279
  add({
@@ -3222,13 +4281,14 @@ function createPaintEditorToolProvider(controller, options = {}) {
3222
4281
  title: "Open a document",
3223
4282
  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
4283
  inputSchema: { type: "object", properties: { document: { type: "object", description: "The saved document JSON." } }, required: ["document"], additionalProperties: false },
3225
- annotations: { title: "Open a document", ...DESTRUCTIVE },
4284
+ annotations: { title: "Open a document", ...DESTRUCTIVE2 },
3226
4285
  handler: ({ document }) => {
3227
4286
  const problems = c.loadJSON(document);
3228
4287
  const d = c.document;
3229
4288
  return { width: d.canvas.width, height: d.canvas.height, layers: c.listLayers(), strokes: d.strokes.length, problems };
3230
4289
  }
3231
4290
  });
4291
+ tools.push(...createPaintGuideTools(c));
3232
4292
  tools.push(...createPigmentTools());
3233
4293
  return { id: options.id ?? "paint-editor", namespace: options.namespace, tools };
3234
4294
  }
@@ -3240,17 +4300,24 @@ export {
3240
4300
  DEFAULT_DPI,
3241
4301
  DEFAULT_DYNAMICS,
3242
4302
  DEFAULT_PAPER,
4303
+ DEFAULT_SWATCHES,
3243
4304
  DEFAULT_VIEW,
3244
4305
  DryStroke,
3245
4306
  GRAIN_PATTERNS,
3246
4307
  GRAIN_TILE,
4308
+ HARMONY_KINDS,
3247
4309
  IDENTITY_AFFINE,
3248
4310
  IDENTITY_COPY,
3249
4311
  MAX_BRUSH_SIZE,
3250
4312
  MAX_CANVAS_SIDE,
4313
+ MAX_COORDINATE,
4314
+ MAX_IMAGE_SIDE,
3251
4315
  MAX_ZOOM,
3252
4316
  MIN_CANVAS_SIDE,
3253
4317
  MIN_ZOOM,
4318
+ MIXING_PAD_HEIGHT,
4319
+ MIXING_PAD_WIDTH,
4320
+ MixingPad,
3254
4321
  PAINT_BRUSH_CATEGORIES,
3255
4322
  PAINT_BRUSH_ENGINES,
3256
4323
  PAINT_FORMAT,
@@ -3265,17 +4332,22 @@ export {
3265
4332
  PaperGrain,
3266
4333
  PenDabWalker,
3267
4334
  PenSmoother,
4335
+ PerspectiveSnapper,
4336
+ SNAP_DECIDE_DISTANCE,
4337
+ SYMMETRY_MODES,
3268
4338
  TRACING_NODE_ID,
3269
4339
  TileableNoise,
3270
4340
  affineCopy,
3271
4341
  applyAffine,
3272
4342
  brushToJSON,
3273
4343
  clonePaper,
4344
+ colorHarmony,
3274
4345
  composeAffine,
3275
4346
  createPaintCompositor,
3276
4347
  createPaintDocument,
3277
4348
  createPaintEditorToolProvider,
3278
4349
  createPaintLayer,
4350
+ defaultGuides,
3279
4351
  defaultImageEncoder,
3280
4352
  deserializePaintDocument,
3281
4353
  deserializePaintDocumentAsync,
@@ -3287,6 +4359,9 @@ export {
3287
4359
  grainHeights,
3288
4360
  hash2,
3289
4361
  hexToRgb8,
4362
+ hslToRgb,
4363
+ hsvToRgb,
4364
+ hueToWheel,
3290
4365
  invertAffine,
3291
4366
  isPalmTouch,
3292
4367
  mapPenPoint,
@@ -3299,25 +4374,36 @@ export {
3299
4374
  normalizeDynamics,
3300
4375
  normalizePaintBrush,
3301
4376
  normalizePaper,
4377
+ normalizePerspective,
3302
4378
  normalizeRotation,
4379
+ normalizeSymmetry,
3303
4380
  normalizeView,
3304
4381
  paperGrain,
3305
4382
  paperKey,
3306
4383
  paperNode,
3307
4384
  paperPreset,
3308
4385
  parseBrushLibrary,
4386
+ parseHexColor,
3309
4387
  penOrientation,
4388
+ perspectiveDirections,
4389
+ perspectiveGuideLines,
3310
4390
  pointerToPenPoint,
3311
4391
  rand01,
3312
4392
  randSigned,
3313
4393
  renderBrushThumbnail,
3314
4394
  renderPaper,
4395
+ rgbToHexColor,
4396
+ rgbToHsl,
4397
+ rgbToHsv,
3315
4398
  samplePressure,
3316
4399
  screenToDocument,
3317
4400
  seedFromString,
3318
4401
  serializePaintDocument,
3319
4402
  serializePaintDocumentExternal,
3320
4403
  strokeBounds,
4404
+ symmetryAxisAngles,
4405
+ symmetryAxisSegments,
4406
+ symmetryCopies,
3321
4407
  thumbnailStroke,
3322
4408
  tiltToOrientation,
3323
4409
  toStrokePoint,
@@ -3325,6 +4411,7 @@ export {
3325
4411
  varyStroke,
3326
4412
  viewCssTransform,
3327
4413
  viewMatrix,
3328
- viewPenToDocument
4414
+ viewPenToDocument,
4415
+ wheelToHue
3329
4416
  };
3330
4417
  //# sourceMappingURL=index.js.map