@pixerate/editor 0.103.0 → 0.105.0

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