@pixerate/editor 0.106.0 → 0.108.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 (70) hide show
  1. package/THIRD_PARTY_NOTICES.md +4 -0
  2. package/dist/animation-editor/index.cjs +39 -39
  3. package/dist/animation-editor/index.d.cts +1 -1
  4. package/dist/animation-editor/index.d.ts +1 -1
  5. package/dist/animation-editor/index.js +1 -1
  6. package/dist/{chunk-DS7KDKPX.js → chunk-3PHA3UKI.js} +2 -2
  7. package/dist/{chunk-ZID226ZG.cjs → chunk-FRD3FUXI.cjs} +172 -172
  8. package/dist/{chunk-ZID226ZG.cjs.map → chunk-FRD3FUXI.cjs.map} +1 -1
  9. package/dist/{chunk-67FRIOPZ.js → chunk-G4BIYE6R.js} +2 -2
  10. package/dist/{chunk-PV42JITT.cjs → chunk-HUFQD4CB.cjs} +44 -44
  11. package/dist/{chunk-PV42JITT.cjs.map → chunk-HUFQD4CB.cjs.map} +1 -1
  12. package/dist/{chunk-63EFQCT5.js → chunk-HUQ7EHVI.js} +1140 -371
  13. package/dist/chunk-HUQ7EHVI.js.map +1 -0
  14. package/dist/{chunk-4X5MIVCE.js → chunk-SOROZ2BF.js} +3 -3
  15. package/dist/{chunk-CRMAXIUV.cjs → chunk-SZ6KMZNM.cjs} +157 -8
  16. package/dist/chunk-SZ6KMZNM.cjs.map +1 -0
  17. package/dist/{chunk-WSTEXEKD.cjs → chunk-T3QZ6S47.cjs} +92 -92
  18. package/dist/{chunk-WSTEXEKD.cjs.map → chunk-T3QZ6S47.cjs.map} +1 -1
  19. package/dist/{chunk-KANRAHTL.cjs → chunk-UTEVPQTS.cjs} +1353 -584
  20. package/dist/chunk-UTEVPQTS.cjs.map +1 -0
  21. package/dist/{chunk-HEW6KDNI.js → chunk-WMBIJCHA.js} +155 -6
  22. package/dist/chunk-WMBIJCHA.js.map +1 -0
  23. package/dist/{compositor-Bc3PNP3C.d.cts → compositor-D92URMlm.d.cts} +1 -1
  24. package/dist/{compositor-BsYYllvR.d.ts → compositor-Kt_M4ruo.d.ts} +1 -1
  25. package/dist/{export-_m3Es4ru.d.cts → export-JGWQEPJn.d.cts} +3 -3
  26. package/dist/{export-iMrop3Jm.d.ts → export-a9bnF7Cl.d.ts} +3 -3
  27. package/dist/image-editor/index.cjs +627 -619
  28. package/dist/image-editor/index.cjs.map +1 -1
  29. package/dist/image-editor/index.d.cts +8 -8
  30. package/dist/image-editor/index.d.ts +8 -8
  31. package/dist/image-editor/index.js +12 -4
  32. package/dist/image-editor/index.js.map +1 -1
  33. package/dist/{mixer-C1M1Nua_.d.cts → mixer-5ziHU8OS.d.cts} +1 -1
  34. package/dist/{mixer-Cu5a9Bzd.d.ts → mixer-DNqiYXYQ.d.ts} +1 -1
  35. package/dist/paint/index.cjs +3 -3
  36. package/dist/paint/index.d.cts +2 -2
  37. package/dist/paint/index.d.ts +2 -2
  38. package/dist/paint/index.js +2 -2
  39. package/dist/paint-editor/index.cjs +365 -132
  40. package/dist/paint-editor/index.cjs.map +1 -1
  41. package/dist/paint-editor/index.d.cts +113 -8
  42. package/dist/paint-editor/index.d.ts +113 -8
  43. package/dist/paint-editor/index.js +249 -16
  44. package/dist/paint-editor/index.js.map +1 -1
  45. package/dist/raster/index.cjs +11 -3
  46. package/dist/raster/index.cjs.map +1 -1
  47. package/dist/raster/index.d.cts +4 -4
  48. package/dist/raster/index.d.ts +4 -4
  49. package/dist/raster/index.js +10 -2
  50. package/dist/{surface-BBJN9wxg.d.ts → surface-CkKOHGPm.d.cts} +20 -2
  51. package/dist/{surface-BBJN9wxg.d.cts → surface-CkKOHGPm.d.ts} +20 -2
  52. package/dist/{svg-Bj1MQFh6.d.cts → svg-ClVIG4T7.d.cts} +26 -2
  53. package/dist/{svg-vme0PLNo.d.ts → svg-SseJTIrA.d.ts} +26 -2
  54. package/dist/vector/index.cjs +977 -948
  55. package/dist/vector/index.cjs.map +1 -1
  56. package/dist/vector/index.d.cts +266 -7
  57. package/dist/vector/index.d.ts +266 -7
  58. package/dist/vector/index.js +427 -398
  59. package/dist/vector/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/skills/pixerate-image-editor/SKILL.md +50 -7
  62. package/skills/pixerate-paint-editor/SKILL.md +21 -1
  63. package/skills/pixerate-vector/SKILL.md +35 -5
  64. package/dist/chunk-63EFQCT5.js.map +0 -1
  65. package/dist/chunk-CRMAXIUV.cjs.map +0 -1
  66. package/dist/chunk-HEW6KDNI.js.map +0 -1
  67. package/dist/chunk-KANRAHTL.cjs.map +0 -1
  68. /package/dist/{chunk-DS7KDKPX.js.map → chunk-3PHA3UKI.js.map} +0 -0
  69. /package/dist/{chunk-67FRIOPZ.js.map → chunk-G4BIYE6R.js.map} +0 -0
  70. /package/dist/{chunk-4X5MIVCE.js.map → chunk-SOROZ2BF.js.map} +0 -0
@@ -4,7 +4,7 @@ import {
4
4
  resolveExternalAssets,
5
5
  serializeLayers,
6
6
  serializeLayersExternal
7
- } from "../chunk-4X5MIVCE.js";
7
+ } from "../chunk-SOROZ2BF.js";
8
8
  import {
9
9
  RasterCompositor,
10
10
  bytesToBase64,
@@ -17,7 +17,7 @@ import {
17
17
  mapLayer,
18
18
  parseDataUrl,
19
19
  pngDataUrl
20
- } from "../chunk-67FRIOPZ.js";
20
+ } from "../chunk-G4BIYE6R.js";
21
21
  import "../chunk-GX332W2C.js";
22
22
  import {
23
23
  CoverageMap,
@@ -35,9 +35,10 @@ import {
35
35
  normalizeBrush,
36
36
  normalizeMixer,
37
37
  rasterizeDab
38
- } from "../chunk-DS7KDKPX.js";
38
+ } from "../chunk-3PHA3UKI.js";
39
39
  import {
40
40
  BLEND_MODES,
41
+ PAINTERLY_BLEND_MODES,
41
42
  TILE,
42
43
  TiledSurface,
43
44
  contentBounds,
@@ -45,12 +46,20 @@ import {
45
46
  fillRect,
46
47
  finishTile,
47
48
  isBlendMode,
49
+ isPainterlyBlendMode,
48
50
  readPixel,
49
51
  readRegion,
52
+ standardBlendMode,
50
53
  surfaceFromPixels,
51
54
  tileKey,
52
55
  tilesInRect
53
- } from "../chunk-HEW6KDNI.js";
56
+ } from "../chunk-WMBIJCHA.js";
57
+ import {
58
+ PerformanceQueue,
59
+ PresenceStore,
60
+ realPresenceClock,
61
+ tweenPresenceCursor
62
+ } from "../chunk-B6DSIEXA.js";
54
63
  import "../chunk-KKREERP2.js";
55
64
 
56
65
  // src/paint-editor/noise.ts
@@ -2713,6 +2722,7 @@ var PaintEditorController = class {
2713
2722
  else this.emit([]);
2714
2723
  return result;
2715
2724
  }
2725
+ /** Checks stroke samples (finite x and y, pressure 0..1) and returns them; throws a sentence naming the bad point. */
2716
2726
  checkPoints(points) {
2717
2727
  if (!Array.isArray(points) || !points.length) throw new PaintEditorError("points is empty; give at least one {x, y} in document pixels.");
2718
2728
  const out = [];
@@ -2936,9 +2946,22 @@ var PaintEditorController = class {
2936
2946
  offset: { x: 0, y: 0 }
2937
2947
  };
2938
2948
  const merged = this.compositePixels({ paper: true }).data;
2939
- const result = exportPsd({ width, height, layers: [paperLayer, ...this.doc.layers], merged, resolution: dpi });
2940
- const pigment = flattenLayers(this.doc.layers).filter((l) => l.blend === "pigment");
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.`);
2949
+ const changed = /* @__PURE__ */ new Map();
2950
+ const toStandard = (layers2) => layers2.map((l) => {
2951
+ const blend = l.blend === "pass-through" ? l.blend : standardBlendMode(l.blend);
2952
+ if (blend !== l.blend) changed.set(l.blend, [...changed.get(l.blend) ?? [], l.name]);
2953
+ const next = blend === l.blend ? l : { ...l, blend };
2954
+ return next.type === "group" ? { ...next, children: toStandard(next.children) } : next;
2955
+ });
2956
+ const layers = toStandard(this.doc.layers);
2957
+ const result = exportPsd({ width, height, layers: [paperLayer, ...layers], merged, resolution: dpi });
2958
+ for (const [mode, names] of changed) {
2959
+ const label = BLEND_MODES.find((m) => m.id === mode)?.label ?? mode;
2960
+ const std = BLEND_MODES.find((m) => m.id === standardBlendMode(mode))?.label ?? standardBlendMode(mode);
2961
+ const list = names.map((n) => `"${n}"`).join(", ");
2962
+ const one = names.length === 1;
2963
+ result.warnings.push(`${label} blending has no PSD equivalent; ${list} ${one ? "was" : "were"} written as ${std}, the nearest Photoshop mode, so ${one ? "it looks" : "they look"} different in Photoshop (the merged image is exact).`);
2964
+ }
2942
2965
  return result;
2943
2966
  }
2944
2967
  /** `render_preview` as a base64 PNG. */
@@ -3234,6 +3257,169 @@ function resampleInto(src, rect, width, height) {
3234
3257
  return { width, height, data: out };
3235
3258
  }
3236
3259
 
3260
+ // src/paint-editor/presence.ts
3261
+ var PaintPresence = class extends PresenceStore {
3262
+ };
3263
+ var FRAME = 16;
3264
+ function strokeSchedule(points) {
3265
+ if (points.length < 2) return points.map(() => 1);
3266
+ const t0 = points[0].t;
3267
+ const t1 = points[points.length - 1].t;
3268
+ if (points.every((p) => Number.isFinite(p.t)) && t1 > t0) {
3269
+ return points.map((p) => Math.min(1, Math.max(0, (p.t - t0) / (t1 - t0))));
3270
+ }
3271
+ const acc = [0];
3272
+ for (let i = 1; i < points.length; i++) acc.push(acc[i - 1] + Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y));
3273
+ const total = acc[acc.length - 1];
3274
+ return total > 0 ? acc.map((d) => d / total) : points.map(() => 1);
3275
+ }
3276
+ function strokeDuration(points, max = 1800) {
3277
+ const t0 = points[0]?.t;
3278
+ const t1 = points[points.length - 1]?.t;
3279
+ let ms;
3280
+ if (points.every((p) => Number.isFinite(p.t)) && t1 > t0) ms = t1 - t0;
3281
+ else {
3282
+ let length = 0;
3283
+ for (let i = 1; i < points.length; i++) length += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
3284
+ ms = length / 1.5;
3285
+ }
3286
+ return Math.min(max, Math.max(250, ms));
3287
+ }
3288
+ function sweepPath(rect, rows = 4) {
3289
+ const inset = Math.min(rect.width, rect.height) * 0.08;
3290
+ const left = rect.x + inset;
3291
+ const right = rect.x + rect.width - inset;
3292
+ const out = [];
3293
+ for (let r = 0; r < rows; r++) {
3294
+ const y = rect.y + inset + (rect.height - 2 * inset) * (r + 0.5) / rows;
3295
+ out.push(r % 2 ? { x: right, y } : { x: left, y }, r % 2 ? { x: left, y } : { x: right, y });
3296
+ }
3297
+ return out;
3298
+ }
3299
+ var centre = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
3300
+ var PaintDirector = class {
3301
+ constructor(controller, presence, options = {}) {
3302
+ this.controller = controller;
3303
+ this.presence = presence;
3304
+ this.options = options;
3305
+ this.clock = options.clock ?? realPresenceClock;
3306
+ this.queue = new PerformanceQueue(presence, { clock: this.clock, linger: options.linger });
3307
+ }
3308
+ controller;
3309
+ presence;
3310
+ options;
3311
+ clock;
3312
+ queue;
3313
+ /** Acts out `commands` in order for `actor`. Resolves with one result per command. */
3314
+ perform(actor, commands) {
3315
+ return this.queue.run(actor, async () => {
3316
+ const results = [];
3317
+ for (const command of commands) {
3318
+ try {
3319
+ results.push({ ok: true, command, result: await this.performOne(actor.id, command) });
3320
+ } catch (error) {
3321
+ results.push({ ok: false, command, message: error instanceof Error ? error.message : String(error) });
3322
+ }
3323
+ }
3324
+ return results;
3325
+ });
3326
+ }
3327
+ ms(n) {
3328
+ return Math.round(n / Math.max(0.1, this.options.speed ?? 1));
3329
+ }
3330
+ where(id) {
3331
+ const a = this.presence.get(id);
3332
+ return { x: a?.x ?? 0, y: a?.y ?? 0 };
3333
+ }
3334
+ /** Glides to `to`; an actor that just appeared starts there instead of the corner. */
3335
+ async glide(id, to, ms = 380) {
3336
+ const a = this.presence.get(id);
3337
+ if (!a || a.x === 0 && a.y === 0) {
3338
+ this.presence.set({ id, x: to.x, y: to.y });
3339
+ return;
3340
+ }
3341
+ await tweenPresenceCursor(this.presence, this.clock, id, this.where(id), to, this.ms(ms));
3342
+ }
3343
+ async say(id, text, ms) {
3344
+ if (!text) return;
3345
+ this.presence.set({ id, message: text });
3346
+ await this.clock.wait(this.ms(ms));
3347
+ }
3348
+ async performOne(id, command) {
3349
+ const c = this.controller;
3350
+ switch (command.kind) {
3351
+ case "say":
3352
+ await this.say(id, command.text, 900);
3353
+ return void 0;
3354
+ case "point":
3355
+ await this.say(id, command.note, 500);
3356
+ await this.glide(id, command.at);
3357
+ this.presence.set({ id, pressed: true });
3358
+ await this.clock.wait(this.ms(110));
3359
+ this.presence.set({ id, pressed: false });
3360
+ return void 0;
3361
+ case "stroke":
3362
+ await this.say(id, command.note, 500);
3363
+ return this.stroke(id, command.stroke);
3364
+ case "fill": {
3365
+ await this.say(id, command.note, 500);
3366
+ const { width, height } = c.getState().document.canvas;
3367
+ const path = sweepPath({ x: 0, y: 0, width, height });
3368
+ await this.glide(id, path[0]);
3369
+ this.presence.set({ id, pressed: true });
3370
+ try {
3371
+ for (let i = 1; i < path.length; i++) await tweenPresenceCursor(this.presence, this.clock, id, path[i - 1], path[i], this.ms(i % 2 ? 260 : 90));
3372
+ } finally {
3373
+ this.presence.set({ id, pressed: false });
3374
+ }
3375
+ return c.fillLayer(command.fill);
3376
+ }
3377
+ case "replay": {
3378
+ await this.say(id, command.note, 500);
3379
+ const source = c.getStrokes().filter((s) => command.replay.strokeIds?.includes(s.id));
3380
+ const first = source[0]?.points[0];
3381
+ if (first) await this.glide(id, first);
3382
+ const result = c.replayStrokes(command.replay);
3383
+ if (result.bounds) await this.glide(id, centre(result.bounds), 520);
3384
+ return result;
3385
+ }
3386
+ }
3387
+ }
3388
+ /** Paints a stroke live: begins it, pushes its points frame by frame as the cursor reaches them, then ends it (one undo step). */
3389
+ async stroke(id, init) {
3390
+ const c = this.controller;
3391
+ const points = c.checkPoints(init.points);
3392
+ const { points: _points, ...rest } = init;
3393
+ const session = c.beginStroke({ snapToPerspective: false, start: points[0], ...rest });
3394
+ try {
3395
+ await this.glide(id, points[0]);
3396
+ this.presence.set({ id, pressed: true });
3397
+ const at = strokeSchedule(points);
3398
+ const duration = this.ms(strokeDuration(points, this.options.maxStrokeMs));
3399
+ let next = 0;
3400
+ const frames = Math.max(1, Math.round(duration / FRAME));
3401
+ for (let f = 1; f <= frames; f++) {
3402
+ const t = f / frames;
3403
+ const from = next;
3404
+ while (next < points.length && at[next] <= t + 1e-9) next++;
3405
+ if (next > from) {
3406
+ session.push(points.slice(from, next));
3407
+ const p = points[next - 1];
3408
+ this.presence.set({ id, x: p.x, y: p.y });
3409
+ }
3410
+ if (f < frames) await this.clock.wait(duration / frames);
3411
+ }
3412
+ if (next < points.length) session.push(points.slice(next));
3413
+ this.presence.set({ id, pressed: false });
3414
+ return session.end();
3415
+ } catch (error) {
3416
+ if (c.activeStroke === session) session.cancel();
3417
+ this.presence.set({ id, pressed: false });
3418
+ throw error;
3419
+ }
3420
+ }
3421
+ };
3422
+
3237
3423
  // src/paint-editor/guide-tools.ts
3238
3424
  var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
3239
3425
  var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
@@ -3585,7 +3771,11 @@ var TIP = {
3585
3771
  smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Position smoothing of the input, 0..0.95." },
3586
3772
  pressureSize: { type: "boolean", description: "Pressure scales the size (down to dynamics.minSize)." },
3587
3773
  pressureOpacity: { type: "boolean", description: "Pressure scales the opacity." },
3588
- blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "How paint combines with the layer. pigment mixes it in like real paint (blue into yellow gives green); multiply darkens like marker ink." }
3774
+ blend: {
3775
+ type: "string",
3776
+ enum: BLEND_MODES.map((m) => m.id),
3777
+ description: "How paint combines with the layer. pigment mixes it in like real paint (blue into yellow gives green); multiply darkens like marker ink; watercolor glazes like a transparent wash; tint, highlight and shadow recolour, light and shade (see list_blend_modes)."
3778
+ }
3589
3779
  },
3590
3780
  additionalProperties: false
3591
3781
  };
@@ -3665,6 +3855,12 @@ function createPaintEditorToolProvider(controller, options = {}) {
3665
3855
  const c = controller;
3666
3856
  const tools = [];
3667
3857
  const add = (t) => tools.push(t);
3858
+ const act = async (command, direct) => {
3859
+ if (!options.director) return direct();
3860
+ const [r] = await options.director.perform(options.actor ?? { id: "agent", name: "Agent" }, [command]);
3861
+ if (!r || !r.ok) throw new Error(r && !r.ok ? r.message : "The director did not run the command.");
3862
+ return r.result;
3863
+ };
3668
3864
  add({
3669
3865
  name: "get_canvas_info",
3670
3866
  title: "Get the canvas",
@@ -3820,7 +4016,11 @@ function createPaintEditorToolProvider(controller, options = {}) {
3820
4016
  name: { type: "string", minLength: 1, description: "New name." },
3821
4017
  visible: { type: "boolean", description: "Show or hide." },
3822
4018
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1." },
3823
- blend: { type: "string", enum: [...BLEND_MODES.map((m) => m.id), "pass-through"], description: "Blend mode (pass-through for groups only)." },
4019
+ blend: {
4020
+ type: "string",
4021
+ enum: [...BLEND_MODES.map((m) => m.id), "pass-through"],
4022
+ description: "Blend mode (pass-through for groups only). Painterly modes: watercolor (the layer glazes like a transparent wash), tint (recolours keeping lightness), highlight (adds soft coloured light), shadow (shades with ambient fill); see list_blend_modes."
4023
+ },
3824
4024
  clipped: { type: "boolean", description: "Clip to the layer below (paint only shows where it has paint)." },
3825
4025
  locks: {
3826
4026
  type: "object",
@@ -3840,6 +4040,22 @@ function createPaintEditorToolProvider(controller, options = {}) {
3840
4040
  annotations: { title: "Change a layer", ...SET2 },
3841
4041
  handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
3842
4042
  });
4043
+ add({
4044
+ name: "list_blend_modes",
4045
+ title: "List blend modes",
4046
+ description: "Lists every layer and brush blend mode: id, label, group (standard Photoshop modes, pigment, or painterly), what the pigment and painterly modes do, and the standard mode PSD export writes in their place. Changes nothing. Read it before choosing a painterly mode for update_layer or a brush tip.",
4047
+ inputSchema: EMPTY2,
4048
+ annotations: { title: "List blend modes", ...READ2 },
4049
+ handler: () => ({
4050
+ modes: BLEND_MODES.map((m) => ({
4051
+ id: m.id,
4052
+ label: m.label,
4053
+ group: m.painterly ? "painterly" : m.id === "pigment" ? "pigment" : "standard",
4054
+ ...m.description ? { description: m.description } : {},
4055
+ psd: standardBlendMode(m.id)
4056
+ }))
4057
+ })
4058
+ });
3843
4059
  add({
3844
4060
  name: "set_active_layer",
3845
4061
  title: "Choose the active layer",
@@ -3904,12 +4120,12 @@ function createPaintEditorToolProvider(controller, options = {}) {
3904
4120
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
3905
4121
  grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
3906
4122
  pressure: { type: "number", minimum: 0, maximum: 1, description: "With grain: how far into the valleys, 0..1 (default 0.5)." },
3907
- blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal; pigment mixes like paint)." }
4123
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal; pigment mixes like paint, watercolor glazes; see list_blend_modes)." }
3908
4124
  },
3909
4125
  additionalProperties: false
3910
4126
  },
3911
4127
  annotations: { title: "Fill a layer", ...WRITE2 },
3912
- handler: (a) => c.fillLayer(a)
4128
+ handler: (a) => act({ kind: "fill", fill: a }, () => c.fillLayer(a))
3913
4129
  });
3914
4130
  add({
3915
4131
  name: "clear_layer",
@@ -4092,7 +4308,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
4092
4308
  add({
4093
4309
  name: "paint_stroke",
4094
4310
  title: "Paint a brush 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.",
4311
+ 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. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. 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.",
4096
4312
  inputSchema: {
4097
4313
  type: "object",
4098
4314
  properties: {
@@ -4108,8 +4324,9 @@ function createPaintEditorToolProvider(controller, options = {}) {
4108
4324
  additionalProperties: false
4109
4325
  },
4110
4326
  annotations: { title: "Paint a brush stroke", ...WRITE2 },
4111
- handler: (a) => {
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 });
4327
+ handler: async (a) => {
4328
+ const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, seed: a.seed, snapToPerspective: a.snapToPerspective };
4329
+ const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
4113
4330
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
4114
4331
  }
4115
4332
  });
@@ -4183,7 +4400,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
4183
4400
  additionalProperties: false
4184
4401
  },
4185
4402
  annotations: { title: "Replay strokes, varied", ...WRITE2 },
4186
- handler: (a) => c.replayStrokes(a)
4403
+ handler: (a) => {
4404
+ const replay = a;
4405
+ return act({ kind: "replay", replay }, () => c.replayStrokes(replay));
4406
+ }
4187
4407
  });
4188
4408
  add({
4189
4409
  name: "set_view",
@@ -4282,7 +4502,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
4282
4502
  add({
4283
4503
  name: "export_psd",
4284
4504
  title: "Export as PSD",
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).',
4505
+ 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 and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',
4286
4506
  inputSchema: EMPTY2,
4287
4507
  annotations: { title: "Export as PSD", ...READ2 },
4288
4508
  handler: () => {
@@ -4591,7 +4811,12 @@ function wheelPick(dx, dy, wheel, color) {
4591
4811
  const [nr, ng, nb] = hsvToRgb(wheelToHue(wheelAngle(dx, dy), wheel), s || 1, v || 1);
4592
4812
  return rgbToHexColor(nr, ng, nb);
4593
4813
  }
4814
+ var BLEND_MENU_GROUPS = [
4815
+ { label: "Standard", modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== "pigment") },
4816
+ { label: "Paint", modes: BLEND_MODES.filter((m) => m.painterly || m.id === "pigment") }
4817
+ ];
4594
4818
  export {
4819
+ BLEND_MENU_GROUPS,
4595
4820
  BLEND_MODES,
4596
4821
  BRUSH_ID,
4597
4822
  BRUSH_ROTATIONS,
@@ -4619,6 +4844,7 @@ export {
4619
4844
  MIXING_PAD_HEIGHT,
4620
4845
  MIXING_PAD_WIDTH,
4621
4846
  MixingPad,
4847
+ PAINTERLY_BLEND_MODES,
4622
4848
  PAINT_BRUSH_CATEGORIES,
4623
4849
  PAINT_BRUSH_ENGINES,
4624
4850
  PAINT_FORMAT,
@@ -4629,10 +4855,12 @@ export {
4629
4855
  PAPER_NODE_ID,
4630
4856
  PAPER_SLIDERS,
4631
4857
  PaintCanvasPresenter,
4858
+ PaintDirector,
4632
4859
  PaintDocumentError,
4633
4860
  PaintEditorController,
4634
4861
  PaintEditorError,
4635
4862
  PaintGesture,
4863
+ PaintPresence,
4636
4864
  PaintStrokeSession,
4637
4865
  PaperGrain,
4638
4866
  PenDabWalker,
@@ -4673,6 +4901,7 @@ export {
4673
4901
  hueToWheel,
4674
4902
  invertAffine,
4675
4903
  isBlendMode,
4904
+ isPainterlyBlendMode,
4676
4905
  isPalmTouch,
4677
4906
  mapPenPoint,
4678
4907
  mix32,
@@ -4713,7 +4942,11 @@ export {
4713
4942
  seedFromString,
4714
4943
  serializePaintDocument,
4715
4944
  serializePaintDocumentExternal,
4945
+ standardBlendMode,
4716
4946
  strokeBounds,
4947
+ strokeDuration,
4948
+ strokeSchedule,
4949
+ sweepPath,
4717
4950
  symmetryAxisAngles,
4718
4951
  symmetryAxisSegments,
4719
4952
  symmetryCopies,