@pixerate/editor 0.246.0 → 0.249.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 (105) hide show
  1. package/README.md +1 -0
  2. package/dist/animation-editor/index.cjs +64 -61
  3. package/dist/animation-editor/index.js +8 -5
  4. package/dist/{brushstrokeRender-ZDHMXJAE.cjs → brushstrokeRender-GCKK7V2I.cjs} +14 -11
  5. package/dist/{brushstrokeRender-AKUI3SEJ.js → brushstrokeRender-T3JZST44.js} +7 -4
  6. package/dist/canvas/index.cjs +8 -8
  7. package/dist/canvas/index.js +1 -1
  8. package/dist/{chunk-UVPD55UZ.cjs → chunk-2SMMZDMG.cjs} +35 -35
  9. package/dist/chunk-2VJAVXLI.js +192 -0
  10. package/dist/{chunk-OLNIPTB3.cjs → chunk-2VX55NFM.cjs} +1 -320
  11. package/dist/chunk-3K6HWEXZ.cjs +92 -0
  12. package/dist/{chunk-UOOK7HTX.js → chunk-3PPJRMIG.js} +1 -1
  13. package/dist/chunk-4D6KP5BV.js +92 -0
  14. package/dist/chunk-5RH3VXAK.js +269 -0
  15. package/dist/{chunk-LDPLEZ5T.js → chunk-6BIDVPED.js} +1 -1
  16. package/dist/{chunk-2ONK3ERD.cjs → chunk-75K3W4D6.cjs} +20 -20
  17. package/dist/chunk-BOHDJJ2O.cjs +192 -0
  18. package/dist/{chunk-2PBZBRP5.js → chunk-D5KGAO4Z.js} +10 -92
  19. package/dist/{chunk-DQWPMGH2.js → chunk-DOIU7VJG.js} +1 -1
  20. package/dist/{chunk-HB4ICNNA.cjs → chunk-DPYAO5U6.cjs} +42 -42
  21. package/dist/chunk-EJLRBS5Q.js +301 -0
  22. package/dist/{chunk-Q2UYUWAZ.cjs → chunk-H4AOUUBE.cjs} +679 -164
  23. package/dist/{chunk-PQ3H2AAN.cjs → chunk-I7NJPHWV.cjs} +122 -204
  24. package/dist/{chunk-FK5LTDOO.cjs → chunk-IOFVM3L3.cjs} +7 -7
  25. package/dist/{chunk-42TWUPNJ.js → chunk-IRSTHLQQ.js} +1 -320
  26. package/dist/{chunk-VDAWA546.js → chunk-J7T5QZ45.js} +614 -99
  27. package/dist/{chunk-VMROIWLL.cjs → chunk-KBYB7OWB.cjs} +3 -3
  28. package/dist/{chunk-5MFDD7SP.cjs → chunk-KYX7NZ5V.cjs} +1 -1
  29. package/dist/chunk-LJRCU3BJ.cjs +301 -0
  30. package/dist/{chunk-XTWXBQD4.cjs → chunk-LP2MCSBE.cjs} +593 -236
  31. package/dist/{chunk-A23PSOBB.js → chunk-O75V4ZS3.js} +1 -1
  32. package/dist/chunk-OTTT5N3K.js +322 -0
  33. package/dist/{chunk-N6WUB3Y5.js → chunk-PZP2SZIZ.js} +2 -2
  34. package/dist/{chunk-4PMCMWQS.js → chunk-UBRJOIR5.js} +367 -10
  35. package/dist/chunk-W5M3J7PV.cjs +322 -0
  36. package/dist/{chunk-4P6H35HU.js → chunk-WA44NKPF.js} +2 -2
  37. package/dist/{chunk-CEPKXLBS.cjs → chunk-WLJZZHUZ.cjs} +23 -23
  38. package/dist/{chunk-P3NIPIKS.js → chunk-WQCNC4DK.js} +1 -1
  39. package/dist/chunk-ZEYMKDYS.cjs +269 -0
  40. package/dist/image-editor/effect-tools.d.cts +37 -4
  41. package/dist/image-editor/effect-tools.d.ts +37 -4
  42. package/dist/image-editor/index.cjs +1340 -1828
  43. package/dist/image-editor/index.d.cts +1 -1
  44. package/dist/image-editor/index.d.ts +1 -1
  45. package/dist/image-editor/index.js +298 -786
  46. package/dist/paint/index.cjs +8 -5
  47. package/dist/paint/index.js +13 -10
  48. package/dist/paint-editor/controller.d.cts +71 -2
  49. package/dist/paint-editor/controller.d.ts +71 -2
  50. package/dist/paint-editor/index.cjs +901 -589
  51. package/dist/paint-editor/index.d.cts +3 -0
  52. package/dist/paint-editor/index.d.ts +3 -0
  53. package/dist/paint-editor/index.js +365 -53
  54. package/dist/paint-editor/procedural.d.cts +82 -5
  55. package/dist/paint-editor/procedural.d.ts +82 -5
  56. package/dist/paint-editor/sticker-art.d.cts +55 -0
  57. package/dist/paint-editor/sticker-art.d.ts +55 -0
  58. package/dist/paint-editor/stickers.d.cts +30 -17
  59. package/dist/paint-editor/stickers.d.ts +30 -17
  60. package/dist/paint-editor/stroke.d.cts +4 -0
  61. package/dist/paint-editor/stroke.d.ts +4 -0
  62. package/dist/paint-editor/tools.d.cts +1 -1
  63. package/dist/paint-editor/tools.d.ts +1 -1
  64. package/dist/paint-editor/ui.d.cts +25 -4
  65. package/dist/paint-editor/ui.d.ts +25 -4
  66. package/dist/{image-editor/layers/effects-ui.d.cts → raster/effects/ui.d.cts} +32 -5
  67. package/dist/{image-editor/layers/effects-ui.d.ts → raster/effects/ui.d.ts} +32 -5
  68. package/dist/raster/index.cjs +17 -3
  69. package/dist/raster/index.d.cts +1 -0
  70. package/dist/raster/index.d.ts +1 -0
  71. package/dist/raster/index.js +23 -9
  72. package/dist/stickers/browser.d.cts +48 -0
  73. package/dist/stickers/browser.d.ts +48 -0
  74. package/dist/stickers/combine.d.cts +10 -0
  75. package/dist/stickers/combine.d.ts +10 -0
  76. package/dist/stickers/conformance.d.cts +18 -0
  77. package/dist/stickers/conformance.d.ts +18 -0
  78. package/dist/stickers/editor.d.cts +14 -0
  79. package/dist/stickers/editor.d.ts +14 -0
  80. package/dist/stickers/errors.d.cts +6 -0
  81. package/dist/stickers/errors.d.ts +6 -0
  82. package/dist/stickers/fake.d.cts +16 -0
  83. package/dist/stickers/fake.d.ts +16 -0
  84. package/dist/stickers/helpers.d.cts +51 -0
  85. package/dist/stickers/helpers.d.ts +51 -0
  86. package/dist/stickers/index.cjs +331 -0
  87. package/dist/stickers/index.d.cts +16 -0
  88. package/dist/stickers/index.d.ts +16 -0
  89. package/dist/stickers/index.js +331 -0
  90. package/dist/stickers/stamps.d.cts +13 -0
  91. package/dist/stickers/stamps.d.ts +13 -0
  92. package/dist/stickers/tools.d.cts +25 -0
  93. package/dist/stickers/tools.d.ts +25 -0
  94. package/dist/stickers/types.d.cts +123 -0
  95. package/dist/stickers/types.d.ts +123 -0
  96. package/dist/vector/index.cjs +978 -975
  97. package/dist/vector/index.js +9 -6
  98. package/package.json +11 -1
  99. package/skills/README.md +1 -0
  100. package/skills/pixerate-paint-editor/SKILL.md +26 -32
  101. package/skills/pixerate-paint-editor/references/gradient-paint.md +23 -0
  102. package/skills/pixerate-paint-editor/references/layer-effects.md +28 -0
  103. package/skills/pixerate-paint-editor/references/light-paint.md +10 -2
  104. package/skills/pixerate-paint-editor/references/particle-brushes.md +66 -0
  105. package/skills/pixerate-stickers/SKILL.md +45 -0
@@ -1,23 +1,36 @@
1
+ import {
2
+ StickerBrowser,
3
+ editorStickers
4
+ } from "../chunk-EJLRBS5Q.js";
1
5
  import {
2
6
  toVideoFfmpeg
3
7
  } from "../chunk-L3JFOTBQ.js";
4
8
  import {
9
+ createLayerEffectTools,
5
10
  deserializeLayers,
6
11
  exportPsd,
7
12
  resolveExternalAssets,
8
13
  serializeLayers,
9
14
  serializeLayersExternal
10
- } from "../chunk-4PMCMWQS.js";
15
+ } from "../chunk-UBRJOIR5.js";
11
16
  import "../chunk-3CI7FRMK.js";
12
17
  import "../chunk-ZNA3IQZF.js";
13
18
  import {
19
+ LAYER_EFFECT_KINDS,
20
+ LayerEffectError,
14
21
  RasterCompositor,
22
+ defaultLayerEffect,
15
23
  findLayer,
16
24
  flattenLayers,
17
25
  insertLayer,
26
+ isLayerEffectKind,
18
27
  isLayerLocked,
19
- mapLayer
20
- } from "../chunk-UOOK7HTX.js";
28
+ layerEffectLabel,
29
+ mapLayer,
30
+ mergeLayerEffect,
31
+ newEffectId,
32
+ validateLayerEffects
33
+ } from "../chunk-3PPJRMIG.js";
21
34
  import "../chunk-OHKOFKBY.js";
22
35
  import "../chunk-ZIF4UJJ6.js";
23
36
  import {
@@ -44,7 +57,7 @@ import {
44
57
  pigmentOf,
45
58
  pointerPressure,
46
59
  wetSupport
47
- } from "../chunk-P3NIPIKS.js";
60
+ } from "../chunk-WQCNC4DK.js";
48
61
  import "../chunk-HVQVHUTV.js";
49
62
  import {
50
63
  writeZip
@@ -55,7 +68,7 @@ import {
55
68
  realPresenceClock,
56
69
  tweenPresenceCursor
57
70
  } from "../chunk-IVSWRVB4.js";
58
- import "../chunk-N6WUB3Y5.js";
71
+ import "../chunk-PZP2SZIZ.js";
59
72
  import {
60
73
  BRUSH_ID,
61
74
  BRUSH_ROTATIONS,
@@ -72,11 +85,15 @@ import {
72
85
  DEFAULT_PARTICLE,
73
86
  DEFAULT_STICKER,
74
87
  DEFAULT_WET_BRUSH,
88
+ DOT_ROTATIONS,
89
+ DOT_TEXTURE_ORDERS,
75
90
  DryStroke,
76
91
  EFFECT_MAP_REPEATS,
77
92
  EFFECT_STRIP_HEIGHT,
78
93
  IDENTITY_COPY,
79
94
  IMPASTO_TOOLS,
95
+ MAX_DOT_TEXTURES,
96
+ MAX_STICKER_STAMPS,
80
97
  MULTI_COLOR_MODES,
81
98
  PAINT_BRUSH_CATEGORIES,
82
99
  PAINT_BRUSH_ENGINES,
@@ -87,13 +104,17 @@ import {
87
104
  ParticleStroke,
88
105
  PenDabWalker,
89
106
  PenSmoother,
90
- STAMP_KINDS,
107
+ RIBBON_PAIRS,
108
+ STICKER_ART_SIZE,
109
+ StickerArtCache,
91
110
  StickerSpray,
92
111
  WET_MEDIA,
93
112
  WET_TOOLS,
113
+ addPolygonArea,
94
114
  brushTakesPaint,
95
115
  brushToJSON,
96
116
  clonePaper,
117
+ defaultStickerRasterizer,
97
118
  drawSticker,
98
119
  effectDabField,
99
120
  effectPaintKey,
@@ -124,16 +145,25 @@ import {
124
145
  readPaintEffectPaint,
125
146
  renderPaper,
126
147
  renderStickers,
148
+ sampleArt,
127
149
  sampleEffectAt,
128
150
  sampleEffectField,
129
- stampCoverage,
130
151
  stickerAt,
131
152
  stickerRect,
153
+ stickerStamp,
132
154
  stickerSurface,
133
155
  tiltToOrientation,
134
156
  toStrokePoint,
135
157
  usesPaintExchange
136
- } from "../chunk-VDAWA546.js";
158
+ } from "../chunk-J7T5QZ45.js";
159
+ import {
160
+ STAMP_KINDS,
161
+ StickersSlot,
162
+ createStickerTools,
163
+ editorStampOf,
164
+ normalizeStickerRef,
165
+ stampCoverage
166
+ } from "../chunk-5RH3VXAK.js";
137
167
  import "../chunk-M6DNZNVY.js";
138
168
  import "../chunk-WEIK3UYB.js";
139
169
  import "../chunk-PWBZH3FK.js";
@@ -174,7 +204,6 @@ import {
174
204
  PaperGrain,
175
205
  SmudgeTip,
176
206
  TileMedium,
177
- TileableNoise,
178
207
  addPaintGradientStop,
179
208
  applyStrokeToTile,
180
209
  blenderDab,
@@ -184,46 +213,51 @@ import {
184
213
  exchangeRates,
185
214
  grainCatch,
186
215
  grainHeights,
187
- hash2,
188
216
  hexToPaintRgb,
189
217
  isGradientPaint,
190
218
  loadMixer,
191
- mix32,
192
219
  mixerRates,
193
220
  normalizeHexColor,
194
221
  paintGradientMappingFor,
195
222
  paintSourceColor,
196
223
  paintSourceCss,
197
224
  parsePaintSource,
198
- rand01,
199
- randSigned,
200
225
  rasterizeDab,
201
226
  readPaintSource,
202
227
  removePaintGradientStop,
203
228
  resolvePaintGradient,
204
229
  reversePaintGradient,
205
230
  scalePaintGradient,
206
- seedFromString,
207
231
  solidPaint,
208
232
  updatePaintGradientStop
209
- } from "../chunk-2PBZBRP5.js";
233
+ } from "../chunk-D5KGAO4Z.js";
234
+ import {
235
+ TileableNoise,
236
+ hash2,
237
+ mix32,
238
+ rand01,
239
+ randSigned,
240
+ seedFromString
241
+ } from "../chunk-4D6KP5BV.js";
210
242
  import "../chunk-ODBT55Q7.js";
211
243
  import {
212
244
  BLEND_MODES,
213
245
  PAINTERLY_BLEND_MODES,
246
+ isBlendMode,
247
+ isPainterlyBlendMode,
248
+ mixPigment8,
249
+ mixPigments,
250
+ standardBlendMode
251
+ } from "../chunk-IRSTHLQQ.js";
252
+ import {
214
253
  base64ToBytes,
215
254
  bytesToBase64,
216
255
  decodePng,
217
256
  encodePng,
218
- isBlendMode,
219
- isPainterlyBlendMode,
220
257
  isPng,
221
- mixPigment8,
222
- mixPigments,
223
258
  parseDataUrl,
224
- pngDataUrl,
225
- standardBlendMode
226
- } from "../chunk-42TWUPNJ.js";
259
+ pngDataUrl
260
+ } from "../chunk-OTTT5N3K.js";
227
261
  import {
228
262
  TILE,
229
263
  TiledSurface,
@@ -1291,7 +1325,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
1291
1325
  };
1292
1326
  if (this.wetDriver) {
1293
1327
  } else if (init.sticker) {
1294
- this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect });
1328
+ this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect, art: init.sticker.art });
1295
1329
  } else if (init.thick) {
1296
1330
  const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
1297
1331
  this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect });
@@ -1501,6 +1535,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
1501
1535
  const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
1502
1536
  const dabs = this.dry ? this.dry.dabCount : this.thickDriver ? this.thickDriver.dabCount : this.stickerSpray ? this.stickerSpray.dabCount : this.wet.dabCount;
1503
1537
  this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };
1538
+ if (this.stickerSpray?.skipped.size) this.result.skippedStickers = [...this.stickerSpray.skipped];
1504
1539
  const carried = this.wet?.load ?? this.thickDriver?.load;
1505
1540
  if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);
1506
1541
  this.host.commit(this, surface, false);
@@ -3500,7 +3535,9 @@ function liveStrokeBounds(stroke) {
3500
3535
  if (q.y > y1) y1 = q.y;
3501
3536
  }
3502
3537
  }
3503
- const pad = stroke.brush.tip.size * (stroke.brush.engine === "particle" ? 2 : 1) + 2;
3538
+ const particle = stroke.brush.engine === "particle" ? stroke.brush.particle : null;
3539
+ const wave = particle ? Math.max(particle.waveAmplitude ?? 0, particle.kind === "fuzzy" ? 0.12 : 0) : 0;
3540
+ const pad = stroke.brush.tip.size * (particle ? 2 + wave : 1) + 2;
3504
3541
  return { x: Math.floor(x0 - pad), y: Math.floor(y0 - pad), width: Math.ceil(x1 - x0 + 2 * pad), height: Math.ceil(y1 - y0 + 2 * pad) };
3505
3542
  }
3506
3543
 
@@ -4032,6 +4069,14 @@ function effectLayersToJSON(map, keep) {
4032
4069
  }
4033
4070
 
4034
4071
  // src/paint-editor/controller.ts
4072
+ function effectGuard(fn) {
4073
+ try {
4074
+ return fn();
4075
+ } catch (e) {
4076
+ if (e instanceof LayerEffectError) throw new PaintEditorError(e.message);
4077
+ throw e;
4078
+ }
4079
+ }
4035
4080
  var PaintEditorError = class extends Error {
4036
4081
  constructor(message) {
4037
4082
  super(message);
@@ -4137,10 +4182,16 @@ var PaintEditorController = class _PaintEditorController {
4137
4182
  wetOp = null;
4138
4183
  /** The effects provider slot (see `setEffectsProvider`). */
4139
4184
  effects = new EffectsSlot();
4185
+ /** The sticker library slot (see `setStickersProvider`). */
4186
+ stickers = new StickersSlot(editorStickers());
4187
+ stickerArt = new StickerArtCache();
4188
+ rasterizeSticker;
4140
4189
  effectsRenderer = new PaintEffectsRenderer();
4141
4190
  paperEffectNodeCache = null;
4142
4191
  constructor(options = {}) {
4143
4192
  this.historyLimit = Math.max(1, options.historyLimit ?? 200);
4193
+ this.rasterizeSticker = options.rasterizeSticker ?? defaultStickerRasterizer;
4194
+ if (options.stickers) this.stickers.set(options.stickers);
4144
4195
  this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });
4145
4196
  this.userBrushes = (options.brushes ?? []).map((b) => normalizePaintBrush({ ...b, builtin: false }));
4146
4197
  const start = this.findBrush(options.brushId ?? DEFAULT_BRUSH_ID) ?? this.findBrush(DEFAULT_BRUSH_ID);
@@ -4557,6 +4608,7 @@ var PaintEditorController = class _PaintEditorController {
4557
4608
  active: l.id === this.doc.activeLayerId,
4558
4609
  bounds,
4559
4610
  ...this.doc.liveStrokes?.[l.id] ? { liveStrokes: this.doc.liveStrokes[l.id].length } : {},
4611
+ ...l.effects?.items.length ? { effects: l.effects.items.length } : {},
4560
4612
  parentId,
4561
4613
  depth
4562
4614
  };
@@ -4630,6 +4682,94 @@ var PaintEditorController = class _PaintEditorController {
4630
4682
  this.commit("Layer properties", { ...this.doc, layers: mapLayer(this.doc.layers, id, () => next) });
4631
4683
  return this.describeLayer(next);
4632
4684
  }
4685
+ // --- Layer effects (layer styles) ---------------------------------------------
4686
+ /** A layer's live effects (an empty, enabled list when it has none). */
4687
+ getLayerEffects(id) {
4688
+ return this.requireLayer(id).effects ?? { enabled: true, items: [] };
4689
+ }
4690
+ /**
4691
+ * Adds a live effect of `kind` (outer and inner glow, drop and inner shadow, stroke, bevel &
4692
+ * emboss, satin, colour / gradient / pattern overlay) with Photoshop's defaults and `settings`
4693
+ * applied, at `index` in the layer's list (default the end). The effect is drawn from the
4694
+ * layer's pixels when it is composited, so it follows later painting. One undo step.
4695
+ */
4696
+ addLayerEffect(id, kind, settings = {}, index) {
4697
+ if (!isLayerEffectKind(kind)) throw new PaintEditorError(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
4698
+ const current = this.effectsLayer(id, "add effects").effects ?? { enabled: true, items: [] };
4699
+ const ids = new Set(current.items.map((e) => e.id));
4700
+ let effectId = newEffectId();
4701
+ while (ids.has(effectId)) effectId = newEffectId();
4702
+ const { id: _id, kind: _kind, ...rest } = settings;
4703
+ const effect = effectGuard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
4704
+ const items = [...current.items];
4705
+ const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
4706
+ items.splice(at, 0, effect);
4707
+ this.writeLayerEffects(id, { ...current, items }, `Add ${layerEffectLabel(kind)}`, true);
4708
+ return { effect, effects: this.getLayerEffects(id) };
4709
+ }
4710
+ /**
4711
+ * Changes one effect: only the given settings (nested objects replace whole; `null` removes an
4712
+ * optional contour or texture). One undo step, or with `commitToHistory` false folded into the
4713
+ * last change of this effect (a slider drag). Returns the layer's effects.
4714
+ */
4715
+ updateLayerEffect(id, effectId, changes, commitToHistory = true) {
4716
+ const current = this.effectsLayer(id, "change its effects").effects;
4717
+ const i = this.requireEffect(id, effectId);
4718
+ const items = [...current.items];
4719
+ items[i] = effectGuard(() => mergeLayerEffect(items[i], changes));
4720
+ this.writeLayerEffects(id, { ...current, items }, `Change ${layerEffectLabel(items[i].kind)}`, commitToHistory);
4721
+ return this.getLayerEffects(id);
4722
+ }
4723
+ /** Removes one effect (one undo step). Returns the layer's remaining effects. */
4724
+ removeLayerEffect(id, effectId) {
4725
+ const current = this.effectsLayer(id, "change its effects").effects;
4726
+ const i = this.requireEffect(id, effectId);
4727
+ this.writeLayerEffects(id, { ...current, items: current.items.filter((e) => e.id !== effectId) }, `Remove ${layerEffectLabel(current.items[i].kind)}`, true);
4728
+ return this.getLayerEffects(id);
4729
+ }
4730
+ /** Turns all of a layer's effects on or off and, when given, sets Scale Effects (0.01..10). One undo step. */
4731
+ setLayerEffectsEnabled(id, enabled, scale, commitToHistory = true) {
4732
+ const current = this.effectsLayer(id, "change its effects").effects ?? { enabled: true, items: [] };
4733
+ if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) throw new PaintEditorError(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
4734
+ const next = { ...current, enabled };
4735
+ if (scale !== void 0) {
4736
+ if (scale === 1) delete next.scale;
4737
+ else next.scale = scale;
4738
+ }
4739
+ this.writeLayerEffects(id, next, "Layer effects", commitToHistory);
4740
+ return this.getLayerEffects(id);
4741
+ }
4742
+ /** Replaces a layer's effects, validated (`null` removes them). One undo step. Returns the effects. */
4743
+ setLayerEffects(id, effects, commitToHistory = true) {
4744
+ this.effectsLayer(id, "change its effects");
4745
+ if (effects) effectGuard(() => validateLayerEffects(effects));
4746
+ this.writeLayerEffects(id, effects, "Layer effects", commitToHistory);
4747
+ return this.getLayerEffects(id);
4748
+ }
4749
+ /** Document patterns for pattern effects besides the built-in ones: none (paint documents bring none). */
4750
+ getDocumentPatterns() {
4751
+ return [];
4752
+ }
4753
+ effectsLayer(id, action) {
4754
+ this.requireIdle("changing layer effects");
4755
+ const l = this.requireLayer(id);
4756
+ if (isLayerLocked(l, "all")) throw new PaintEditorError(`Layer "${l.name}" is fully locked, so it can't ${action}; unlock it first (locks {all: false}).`);
4757
+ return l;
4758
+ }
4759
+ requireEffect(id, effectId) {
4760
+ const l = this.requireLayer(id);
4761
+ const items = l.effects?.items ?? [];
4762
+ const i = items.findIndex((e) => e.id === effectId);
4763
+ if (i === -1) throw new PaintEditorError(`Layer "${l.name}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
4764
+ return i;
4765
+ }
4766
+ /** Writes a layer's effects (dropped when empty, enabled and unscaled), as an undo step or folded into the last one. */
4767
+ writeLayerEffects(id, effects, label, commitToHistory) {
4768
+ const next = { ...this.requireLayer(id) };
4769
+ if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) next.effects = effects;
4770
+ else delete next.effects;
4771
+ this.commitOrAmend(label, { ...this.doc, layers: mapLayer(this.doc.layers, id, () => next) }, commitToHistory);
4772
+ }
4633
4773
  removeLayer(id) {
4634
4774
  this.requireIdle("removing a layer");
4635
4775
  const l = this.requireLayer(id);
@@ -5210,6 +5350,7 @@ var PaintEditorController = class _PaintEditorController {
5210
5350
  else delete next.builtin;
5211
5351
  this.brush = next;
5212
5352
  this.emit([]);
5353
+ if (next.engine === "sticker" && next.sticker.stamps.some((r) => !editorStampOf(r))) void this.prepareStickers(next.sticker.stamps).catch(() => this.emit([]));
5213
5354
  return this.brush;
5214
5355
  }
5215
5356
  /** Saves the active brush (or `brush`) into the user library under `id` / `name`. */
@@ -5535,7 +5676,7 @@ var PaintEditorController = class _PaintEditorController {
5535
5676
  thick: brush.engine === "impasto" ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
5536
5677
  // Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
5537
5678
  light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
5538
- sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc) } : void 0,
5679
+ sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
5539
5680
  paint: effect ? null : this.strokeGradient(init, brush, color),
5540
5681
  effect
5541
5682
  });
@@ -6332,9 +6473,19 @@ var PaintEditorController = class _PaintEditorController {
6332
6473
  const stickers = {};
6333
6474
  for (const [id, list] of Object.entries(ext.stickers)) {
6334
6475
  if (!findLayer(state.layers, id) || !Array.isArray(list)) continue;
6335
- const ok = list.filter((x) => x && typeof x.id === "string" && STAMP_KINDS.includes(x.stamp) && Number.isFinite(x.x) && Number.isFinite(x.y) && x.size > 0);
6476
+ const ok = list.flatMap((x) => {
6477
+ if (!x || typeof x.id !== "string" || !Number.isFinite(x.x) || !Number.isFinite(x.y) || !(x.size > 0)) return [];
6478
+ try {
6479
+ const ref2 = normalizeStickerRef(x.ref ?? x.stamp);
6480
+ const stamp = editorStampOf(ref2);
6481
+ const { stamp: _old, ...rest } = x;
6482
+ return [{ ...rest, ref: ref2, ...stamp ? { stamp } : {} }];
6483
+ } catch {
6484
+ return [];
6485
+ }
6486
+ });
6336
6487
  if (ok.length !== list.length) problems.push(`Some stickers of layer "${id}" could not be read; they were dropped.`);
6337
- stickers[id] = ok.map((x) => ({ ...x }));
6488
+ stickers[id] = ok;
6338
6489
  }
6339
6490
  if (Object.keys(stickers).length) state = { ...state, stickers };
6340
6491
  }
@@ -6438,6 +6589,8 @@ var PaintEditorController = class _PaintEditorController {
6438
6589
  delete this.extensions.paperEffect;
6439
6590
  this.viewCompositor.clear();
6440
6591
  this.emit();
6592
+ const refs = Object.values(this.doc.stickers ?? {}).flatMap((list) => list.map((x) => x.ref));
6593
+ if (refs.some((r) => !editorStampOf(r))) void this.prepareStickers(refs).catch(() => this.emit([]));
6441
6594
  return loaded.problems;
6442
6595
  }
6443
6596
  /** Opens a saved document (clears the history). Returns what could not be read. */
@@ -6765,6 +6918,56 @@ var PaintEditorController = class _PaintEditorController {
6765
6918
  if (!stickers) throw new PaintEditorError(`Layer "${l.name}" has no stickers; spray some with a sticker brush (list_brushes category sticker).`);
6766
6919
  return { layer: l, stickers };
6767
6920
  }
6921
+ // --- The sticker library ---------------------------------------------------------
6922
+ /** The sticker library: the editor's own stamps unless a host set its own. */
6923
+ get stickersProvider() {
6924
+ return this.stickers.provider;
6925
+ }
6926
+ /**
6927
+ * Sets the sticker library (`null` goes back to the editor's own stamps, `editorStickers()`);
6928
+ * to offer both, pass `combineStickerProviders([editorStickers(), yours])`. Loaded art is
6929
+ * forgotten. Not an edit: no undo step.
6930
+ */
6931
+ setStickersProvider(provider) {
6932
+ if ((provider ?? this.stickers.fallback) === this.stickers.provider) return;
6933
+ this.stickers.set(provider);
6934
+ this.stickerArt.clear();
6935
+ this.emit([]);
6936
+ }
6937
+ /**
6938
+ * Loads the art of `refs` (provider stickers; editor stamps need none) so sprays and placements
6939
+ * can draw them. Rejects with a `PaintEditorError` naming an unknown ref and the closest known
6940
+ * ones, or a sticker whose art failed. Emits when art arrives, so views redraw.
6941
+ */
6942
+ async prepareStickers(refs) {
6943
+ const list = [...new Set(refs.map((r) => this.readStickerRef(r)))].filter((r) => !editorStampOf(r));
6944
+ try {
6945
+ await Promise.all(list.map((r) => this.stickerArt.ensure(this.stickers.provider, r, this.rasterizeSticker)));
6946
+ } catch (e) {
6947
+ throw new PaintEditorError(e instanceof Error ? e.message : String(e));
6948
+ } finally {
6949
+ if (list.length) this.emit([]);
6950
+ }
6951
+ }
6952
+ /** Whether a sticker's art is ready to draw: `ready` (always for editor stamps), `loading`, `failed` (with why) or `none`. */
6953
+ stickerArtStatus(ref2) {
6954
+ return this.stickerArt.status(this.readStickerRef(ref2));
6955
+ }
6956
+ readStickerRef(ref2) {
6957
+ try {
6958
+ return normalizeStickerRef(ref2);
6959
+ } catch (e) {
6960
+ throw new PaintEditorError(e.message);
6961
+ }
6962
+ }
6963
+ /** Refuses to redraw stickers whose art has not loaded (they would vanish), naming them and what to call. */
6964
+ requireStickerArt(stickers) {
6965
+ const missing = [...new Set(stickers.filter((s) => !stickerStamp(s) && !this.stickerArt.get(s.ref)).map((s) => s.ref))];
6966
+ if (missing.length) {
6967
+ for (const r of missing) void this.stickerArt.ensure(this.stickers.provider, r, this.rasterizeSticker).then(() => this.emit([]), () => this.emit([]));
6968
+ throw new PaintEditorError(`The art for ${missing.map((r) => `"${r}"`).join(", ")} has not loaded yet; it is loading now (prepareStickers), so try again when it has.`);
6969
+ }
6970
+ }
6768
6971
  /** A layer's stickers, bottom first. */
6769
6972
  listStickers(layerId) {
6770
6973
  const id = layerId ?? this.doc.activeLayerId;
@@ -6778,6 +6981,7 @@ var PaintEditorController = class _PaintEditorController {
6778
6981
  */
6779
6982
  commitStickers(label, layer, stickers, dirty) {
6780
6983
  if (isLayerLocked(layer, "pixels")) throw new PaintEditorError(`Layer "${layer.name}" has its pixels locked; unlock it first.`);
6984
+ this.requireStickerArt(stickers);
6781
6985
  const before = this.previewBase() ?? this.doc;
6782
6986
  this.stickerPreview = null;
6783
6987
  const surface = this.redrawStickers(layer, stickers, dirty);
@@ -6786,7 +6990,7 @@ var PaintEditorController = class _PaintEditorController {
6786
6990
  }
6787
6991
  redrawStickers(layer, stickers, dirty) {
6788
6992
  const partial = dirty && !layer.offset.x && !layer.offset.y;
6789
- return partial ? stickerSurface(stickers, layer.surface, dirty) : stickerSurface(stickers);
6993
+ return partial ? stickerSurface(stickers, layer.surface, dirty, this.stickerArt.get) : stickerSurface(stickers, void 0, void 0, this.stickerArt.get);
6790
6994
  }
6791
6995
  /** The document from before the sticker preview on screen, or `null` when none is (anything else changed the document since). */
6792
6996
  previewBase() {
@@ -6805,7 +7009,7 @@ var PaintEditorController = class _PaintEditorController {
6805
7009
  const id = layerId ?? this.doc.activeLayerId;
6806
7010
  if (!id) return null;
6807
7011
  this.requireLayer(id);
6808
- const hit = stickerAt(this.doc.stickers?.[id] ?? [], x, y);
7012
+ const hit = stickerAt(this.doc.stickers?.[id] ?? [], x, y, this.stickerArt.get);
6809
7013
  return hit ? { ...hit } : null;
6810
7014
  }
6811
7015
  /** Picks a sticker for the panel and canvas (`null` for none). Editor state, not an undo step. */
@@ -6859,7 +7063,7 @@ var PaintEditorController = class _PaintEditorController {
6859
7063
  */
6860
7064
  addStickers(layerId, input) {
6861
7065
  const layer = this.wetLayer(layerId, "add stickers to");
6862
- if (!input.length) throw new PaintEditorError("Pass at least one sticker (stamp, x, y).");
7066
+ if (!input.length) throw new PaintEditorError("Pass at least one sticker (ref, x, y).");
6863
7067
  if (input.length > MAX_ADD_STICKERS) throw new PaintEditorError(`At most ${MAX_ADD_STICKERS} stickers per call; split them over several calls.`);
6864
7068
  const existing = this.doc.stickers?.[layer.id];
6865
7069
  if (!existing?.length) {
@@ -6867,7 +7071,8 @@ var PaintEditorController = class _PaintEditorController {
6867
7071
  }
6868
7072
  let n = this.nextStickerNumber();
6869
7073
  const added = input.map((raw, i) => {
6870
- for (const k of ["stamp", "x", "y"]) if (raw[k] === void 0) throw new PaintEditorError(`Sticker ${i} needs ${k}.`);
7074
+ if (raw.ref === void 0 && raw.stamp === void 0) throw new PaintEditorError(`Sticker ${i} needs ref (a sticker ref from find_stickers, e.g. "editor:star").`);
7075
+ for (const k of ["x", "y"]) if (raw[k] === void 0) throw new PaintEditorError(`Sticker ${i} needs ${k}.`);
6871
7076
  let u;
6872
7077
  try {
6873
7078
  u = this.readStickerUpdate(raw);
@@ -7932,7 +8137,7 @@ function createPaintStickerTools(c) {
7932
8137
  {
7933
8138
  name: "list_stickers",
7934
8139
  title: "List stickers",
7935
- description: "Lists a layer\u2019s stickers bottom first: id, stamp, x, y (centre, document pixels), size (diameter px), rotation (degrees clockwise), color, opacity. Empty for a layer without stickers. Changes nothing.",
8140
+ description: "Lists a layer\u2019s stickers bottom first: id, ref (provider:id; stamp too for the editor\u2019s own), x, y (centre, document pixels), size (diameter px), rotation (degrees clockwise), color, opacity. Empty for a layer without stickers. Changes nothing.",
7936
8141
  inputSchema: { type: "object", properties: { layerId: LAYER_ID }, additionalProperties: false },
7937
8142
  annotations: { title: "List stickers", ...READ },
7938
8143
  coordinates: { out: ["stickers[]"] },
@@ -7959,7 +8164,7 @@ function createPaintStickerTools(c) {
7959
8164
  {
7960
8165
  name: "add_stickers",
7961
8166
  title: "Place stickers",
7962
- description: "Places stickers exactly where you say, on top of a sticker layer or onto an empty layer (which becomes a sticker layer). Use it for deliberate placement; spray with paint_stroke and a sticker brush for a random scatter. One undo step. Returns the new stickers with their ids, for update_sticker, arrange_stickers and remove_stickers.",
8167
+ description: "Places stickers exactly where you say, on top of a sticker layer or onto an empty layer (which becomes a sticker layer): any sticker from the sticker library by ref (find_stickers), its art loaded first; tintable ones take color. Use it for deliberate placement; spray with paint_stroke and a sticker brush for a random scatter. An unknown ref is refused with the closest ones. One undo step. Returns the new stickers with their ids, for update_sticker, arrange_stickers and remove_stickers.",
7963
8168
  inputSchema: {
7964
8169
  type: "object",
7965
8170
  properties: {
@@ -7972,7 +8177,8 @@ function createPaintStickerTools(c) {
7972
8177
  items: {
7973
8178
  type: "object",
7974
8179
  properties: {
7975
- stamp: { type: "string", enum: [...STAMP_KINDS], description: "The stamp." },
8180
+ ref: { type: "string", minLength: 1, description: "The sticker: a ref provider:id from find_stickers (e.g. editor:star). Give ref or stamp." },
8181
+ stamp: { type: "string", enum: [...STAMP_KINDS], description: "Older form: an editor stamp (same as ref editor:<stamp>)." },
7976
8182
  x: { type: "number", description: "Centre x in document pixels." },
7977
8183
  y: { type: "number", description: "Centre y in document pixels." },
7978
8184
  size: { type: "number", minimum: 2, maximum: 2048, description: "Diameter in pixels (default 48)." },
@@ -7981,7 +8187,7 @@ function createPaintStickerTools(c) {
7981
8187
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
7982
8188
  seed: { type: "integer", minimum: 0, description: "Texture seed (default from the new id); splat and confetti shapes vary with it." }
7983
8189
  },
7984
- required: ["stamp", "x", "y"],
8190
+ required: ["x", "y"],
7985
8191
  additionalProperties: false
7986
8192
  }
7987
8193
  }
@@ -7991,7 +8197,14 @@ function createPaintStickerTools(c) {
7991
8197
  },
7992
8198
  annotations: { title: "Place stickers", ...WRITE },
7993
8199
  coordinates: { in: ["stickers[]"], out: ["stickers[]"] },
7994
- handler: ({ layerId, stickers }) => ({ layerId: layerId ?? c.document.activeLayerId, stickers: c.addStickers(layerId, stickers) })
8200
+ handler: async ({ layerId, stickers }) => {
8201
+ const list = stickers;
8202
+ list.forEach((x, i) => {
8203
+ if (x.ref === void 0 && x.stamp === void 0) throw new Error(`Sticker ${i} needs ref (a sticker ref from find_stickers, e.g. "editor:star").`);
8204
+ });
8205
+ await c.prepareStickers(list.map((x) => x.ref ?? `editor:${x.stamp}`));
8206
+ return { layerId: layerId ?? c.document.activeLayerId, stickers: c.addStickers(layerId, list) };
8207
+ }
7995
8208
  },
7996
8209
  {
7997
8210
  name: "update_sticker",
@@ -8002,7 +8215,8 @@ function createPaintStickerTools(c) {
8002
8215
  properties: {
8003
8216
  layerId: LAYER_ID,
8004
8217
  id: { type: "string", description: "Sticker id from list_stickers." },
8005
- stamp: { type: "string", enum: [...STAMP_KINDS], description: "Another stamp." },
8218
+ ref: { type: "string", minLength: 1, description: "Another sticker: a ref provider:id from find_stickers." },
8219
+ stamp: { type: "string", enum: [...STAMP_KINDS], description: "Older form: another editor stamp (same as ref editor:<stamp>)." },
8006
8220
  x: { type: "number", description: "Centre x in document pixels." },
8007
8221
  y: { type: "number", description: "Centre y in document pixels." },
8008
8222
  size: { type: "number", minimum: 2, maximum: 2048, description: "Diameter in pixels." },
@@ -8015,7 +8229,11 @@ function createPaintStickerTools(c) {
8015
8229
  },
8016
8230
  annotations: { title: "Change a sticker", ...WRITE },
8017
8231
  coordinates: { in: [""], out: [""] },
8018
- handler: ({ layerId, id, ...update }) => c.updateSticker(layerId, id, update)
8232
+ handler: async ({ layerId, id, ...update }) => {
8233
+ const refs = c.listStickers(layerId).map((s) => s.ref);
8234
+ await c.prepareStickers(update.ref ? [...refs, update.ref] : refs);
8235
+ return c.updateSticker(layerId, id, update);
8236
+ }
8019
8237
  },
8020
8238
  {
8021
8239
  name: "arrange_stickers",
@@ -8032,7 +8250,10 @@ function createPaintStickerTools(c) {
8032
8250
  additionalProperties: false
8033
8251
  },
8034
8252
  annotations: { title: "Bring stickers forward or back", ...WRITE },
8035
- handler: ({ layerId, ids, to }) => ({ order: c.arrangeStickers(layerId, ids, to) })
8253
+ handler: async ({ layerId, ids, to }) => {
8254
+ await c.prepareStickers(c.listStickers(layerId).map((s) => s.ref));
8255
+ return { order: c.arrangeStickers(layerId, ids, to) };
8256
+ }
8036
8257
  },
8037
8258
  {
8038
8259
  name: "remove_stickers",
@@ -8040,7 +8261,10 @@ function createPaintStickerTools(c) {
8040
8261
  description: "Deletes stickers by id, or every sticker on the layer when ids is missing. One undo step. Returns the ids removed.",
8041
8262
  inputSchema: { type: "object", properties: { layerId: LAYER_ID, ids: { type: "array", items: { type: "string", description: "Sticker id." }, description: "Sticker ids (default all)." } }, additionalProperties: false },
8042
8263
  annotations: { title: "Delete stickers", ...DESTRUCTIVE },
8043
- handler: ({ layerId, ids }) => ({ removed: c.removeStickers(layerId, ids) })
8264
+ handler: async ({ layerId, ids }) => {
8265
+ await c.prepareStickers(c.listStickers(layerId).map((s) => s.ref));
8266
+ return { removed: c.removeStickers(layerId, ids) };
8267
+ }
8044
8268
  }
8045
8269
  ];
8046
8270
  }
@@ -9001,17 +9225,39 @@ var PARTICLE_BRUSH = {
9001
9225
  type: "object",
9002
9226
  description: "Particle engine settings (procedural brushes). Missing fields keep their current values.",
9003
9227
  properties: {
9004
- kind: { type: "string", enum: [...PARTICLE_KINDS], description: "string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade." },
9005
- render: { type: "string", enum: [...PARTICLE_RENDERS], description: "dot: a mark at each step; line: each particle draws a continuous line; sequence: beads spaced along each path." },
9228
+ kind: {
9229
+ type: "string",
9230
+ enum: [...PARTICLE_KINDS],
9231
+ description: "string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade; elastic: strands whose links keep a rest length, stretching when the pen is fast and snapping back; fuzzy: winding strings, each strand waving out of step with the others (always waves by at least 0.12 of the tip size)."
9232
+ },
9233
+ render: {
9234
+ type: "string",
9235
+ enum: [...PARTICLE_RENDERS],
9236
+ description: "dot: a mark at each step (round, or dotTextures); line: each particle draws a continuous line; sequence: beads spaced along each path (round, or dotTextures); ribbon: the band between neighbouring particles' paths filled (ribbonFill), with the paths drawn as its edges at the mark size (flame, silk, smoke)."
9237
+ },
9006
9238
  count: { type: "integer", minimum: 1, maximum: 64, description: "Particles (flow: how many are thrown per tip of travel)." },
9007
- size: { type: "number", minimum: 0, maximum: 1, description: "Mark size as a fraction of the tip size." },
9008
- spread: { type: "number", minimum: 0, maximum: 1, description: "How far apart the particles start, as a fraction of the tip size." },
9009
- stiffness: { type: "number", minimum: 0, maximum: 1, description: "Spring and string stiffness: stiff follows closely, soft swings wide." },
9239
+ size: { type: "number", minimum: 0, maximum: 1, description: "Mark size as a fraction of the tip size (ribbon: the width of its edges)." },
9240
+ spread: { type: "number", minimum: 0, maximum: 1, description: "How far apart the particles start, as a fraction of the tip size (ribbon: how wide it is)." },
9241
+ stiffness: { type: "number", minimum: 0, maximum: 1, description: "Spring, string and elastic stiffness: stiff follows closely (elastic: snaps back faster), soft swings wide." },
9010
9242
  damping: { type: "number", minimum: 0, maximum: 1, description: "How quickly motion dies down." },
9011
9243
  speed: { type: "number", minimum: 0, maximum: 1, description: "Orbit and flow speed." },
9012
9244
  gravity: { type: "number", minimum: -1, maximum: 1, description: "Pull downwards (negative: upwards)." },
9013
9245
  life: { type: "number", minimum: 0, maximum: 1, description: "Flow particles: how long they live." },
9014
- interval: { type: "number", minimum: 0, maximum: 1, description: "Sequence: spacing of the beads." }
9246
+ interval: { type: "number", minimum: 0, maximum: 1, description: "Sequence: spacing of the beads." },
9247
+ ribbonFill: { type: "number", minimum: 0, maximum: 1, description: "Ribbon: opacity of the fill between the paths, relative to their edges (default 0.5; 0 draws only the edges)." },
9248
+ ribbonPairs: { type: "string", enum: [...RIBBON_PAIRS], description: "Ribbon: adjacent (default) joins each particle to its neighbour around the pen; first-last makes one band between the outermost two." },
9249
+ waveAmplitude: { type: "number", minimum: 0, maximum: 1, description: "Wave: how far each particle's drawn place swings sideways to the stroke, as a fraction of the tip size (default 0: no wave). Works with every kind and render." },
9250
+ waveLength: { type: "number", minimum: 4, maximum: 2e3, description: "Wave: wavelength in px of pen travel (default 60)." },
9251
+ waveSpeed: { type: "number", minimum: 0, maximum: 1, description: "Wave: how fast the waves run, by stroke time (0 default: they stand still on the path; 1: about four a second, so slow strokes wave more). Strokes without times run at a nominal 0.5 px/ms." },
9252
+ wavePhaseJitter: { type: "number", minimum: 0, maximum: 1, description: "Wave: how far out of step the particles' waves are (default 0: all in step, a wavy band; 1: random phases, tangled strands)." },
9253
+ dotTextures: {
9254
+ type: "array",
9255
+ maxItems: MAX_DOT_TEXTURES,
9256
+ items: { type: "string", enum: [...STAMP_KINDS], description: "A stamp." },
9257
+ description: `Dot and sequence renders: up to ${MAX_DOT_TEXTURES} sticker stamps (list_stickers) drawn instead of round marks; repeats are allowed (they weight the order). Empty (default): round marks.`
9258
+ },
9259
+ dotTextureOrder: { type: "string", enum: [...DOT_TEXTURE_ORDERS], description: "Textured dots: sequence (default) uses the stamps in turn; random picks one per dot (seeded, so replays are exact)." },
9260
+ dotRotation: { type: "string", enum: [...DOT_ROTATIONS], description: "Textured dots: follow turns each with its particle's motion; random (default) turns each at random; fixed uses the tip angle." }
9015
9261
  },
9016
9262
  additionalProperties: false
9017
9263
  };
@@ -9032,7 +9278,13 @@ var STICKER_BRUSH = {
9032
9278
  type: "object",
9033
9279
  description: "Sticker engine settings: stamps sprayed along the stroke as editable stickers. Missing fields keep their current values.",
9034
9280
  properties: {
9035
- stamps: { type: "array", minItems: 1, maxItems: 9, items: { type: "string", enum: [...STAMP_KINDS], description: "A stamp." }, description: "The stamps to spray (one picked at random per sticker)." },
9281
+ stamps: {
9282
+ type: "array",
9283
+ minItems: 1,
9284
+ maxItems: MAX_STICKER_STAMPS,
9285
+ items: { type: "string", minLength: 1, description: "A sticker ref provider:id from find_stickers (e.g. editor:star); a bare editor stamp (star) means editor:star." },
9286
+ description: `The stickers to spray, one picked at random per sticker: refs from find_stickers (the editor's own: ${STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}). Each is checked against the sticker library and an unknown ref is refused with the closest ones. A provider's sticker is drawn from its art (tinted to the paint colour when tintable), loaded before the stroke.`
9287
+ },
9036
9288
  density: { type: "number", minimum: 0, maximum: 1, description: "How closely stickers follow each other: about density \xD7 4 stickers per tip diameter of travel (0.25 \u2248 one per diameter, 1 \u2248 four). Default 0.4." },
9037
9289
  sizeJitter: { type: "number", minimum: 0, maximum: 1, description: "Random size change." },
9038
9290
  rotationJitter: { type: "number", minimum: 0, maximum: 1, description: "Random turn as a fraction of \xB1180\xB0." },
@@ -9431,7 +9683,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
9431
9683
  add({
9432
9684
  name: "list_brushes",
9433
9685
  title: "List brushes",
9434
- description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, glow wash, highlighter; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
9686
+ description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, light ribbon, flame ribbon, neon strand, smoke ribbon, bokeh dots, twinkle sparks, glow wash, highlighter; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
9435
9687
  inputSchema: EMPTY3,
9436
9688
  annotations: { title: "List brushes", ...READ6 },
9437
9689
  handler: () => {
@@ -9459,7 +9711,11 @@ function createPaintEditorToolProvider(controller, options = {}) {
9459
9711
  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.",
9460
9712
  inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes to start from." }, ...BRUSH_FIELDS }, additionalProperties: false },
9461
9713
  annotations: { title: "Set the brush", ...SET4 },
9462
- handler: (a) => brushToJSON(c.setBrush(a))
9714
+ handler: async (a) => {
9715
+ const stamps = a.sticker?.stamps;
9716
+ if (stamps) await c.prepareStickers(stamps);
9717
+ return brushToJSON(c.setBrush(a));
9718
+ }
9463
9719
  });
9464
9720
  add({
9465
9721
  name: "save_brush",
@@ -9635,6 +9891,9 @@ function createPaintEditorToolProvider(controller, options = {}) {
9635
9891
  coordinates: { in: ["points[]"], out: ["bounds"] },
9636
9892
  handler: async (a) => {
9637
9893
  await c.wetIdle();
9894
+ const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
9895
+ const over = a.brush;
9896
+ if ((over?.engine ?? base?.engine) === "sticker") await c.prepareStickers(over?.sticker?.stamps ?? base?.sticker.stamps ?? []);
9638
9897
  const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush: a.brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
9639
9898
  const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
9640
9899
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
@@ -9943,6 +10202,20 @@ function createPaintEditorToolProvider(controller, options = {}) {
9943
10202
  tools.push(...createPaintReplayTools(c));
9944
10203
  tools.push(...createPaintStickerTools(c));
9945
10204
  tools.push(...createPigmentTools());
10205
+ tools.push(...createStickerTools({ provider: () => c.stickersProvider }));
10206
+ tools.push(
10207
+ ...createLayerEffectTools(
10208
+ {
10209
+ requireLayer: (id) => void c.getLayerEffects(id),
10210
+ getLayerEffects: (id) => c.getLayerEffects(id),
10211
+ addLayerEffect: (id, kind, settings, index) => c.addLayerEffect(id, kind, settings, index),
10212
+ updateLayerEffect: (id, effectId, changes) => c.updateLayerEffect(id, effectId, changes),
10213
+ removeLayerEffect: (id, effectId) => c.removeLayerEffect(id, effectId),
10214
+ setLayerEffectsEnabled: (id, enabled, scale) => c.setLayerEffectsEnabled(id, enabled, scale)
10215
+ },
10216
+ { layers: "a pixel, stroke, sticker or group layer", error: (m) => new PaintEditorError(m) }
10217
+ )
10218
+ );
9946
10219
  const exclude = new Set(options.exclude ?? []);
9947
10220
  const description = "Natural-media painting: paper and grain, dry and wet brushes, the brush library, strokes as data, pigment mixing, stencils, guides, layers and export.";
9948
10221
  return withPaintPlayback({
@@ -10103,7 +10376,7 @@ var PaintGesture = class {
10103
10376
  }
10104
10377
  const brush = this.controller.getState().brush;
10105
10378
  const size = Math.min(2048, Math.max(2, Math.round(brush.tip.size)));
10106
- const [added] = this.controller.addStickers(void 0, [{ stamp: brush.sticker.stamps[0] ?? "star", x: round2(p.x), y: round2(p.y), size }]);
10379
+ const [added] = this.controller.addStickers(void 0, [{ ref: brush.sticker.stamps[0] ?? "editor:star", x: round2(p.x), y: round2(p.y), size }]);
10107
10380
  this.controller.selectSticker(added.id);
10108
10381
  } catch (err) {
10109
10382
  this.error = err instanceof Error ? err.message : String(err);
@@ -10413,7 +10686,12 @@ function engineControls(brush) {
10413
10686
  { path: "particle.speed", label: "Speed", min: 0, max: 1, step: 0.01 },
10414
10687
  { path: "particle.gravity", label: "Gravity", min: -1, max: 1, step: 0.01 },
10415
10688
  { path: "particle.life", label: "Life", min: 0, max: 1, step: 0.01 },
10416
- { path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 }
10689
+ { path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 },
10690
+ { path: "particle.ribbonFill", label: "Ribbon fill", min: 0, max: 1, step: 0.01 },
10691
+ { path: "particle.waveAmplitude", label: "Wave", min: 0, max: 1, step: 0.01 },
10692
+ { path: "particle.waveLength", label: "Wavelength", min: 4, max: 400, step: 1 },
10693
+ { path: "particle.waveSpeed", label: "Wave speed", min: 0, max: 1, step: 0.01 },
10694
+ { path: "particle.wavePhaseJitter", label: "Wave scatter", min: 0, max: 1, step: 0.01 }
10417
10695
  ],
10418
10696
  mixingPath: null,
10419
10697
  dirty: false,
@@ -10628,13 +10906,29 @@ var LIGHT_SLIDERS = [
10628
10906
  { path: "elevation", label: "Light height", min: 5, max: 90, step: 1 },
10629
10907
  { path: "relief", label: "Relief", min: 0, max: 1, step: 0.01 }
10630
10908
  ];
10631
- var PARTICLE_KIND_LABELS = { string: "Strings", spring: "Springs", orbit: "Orbits", flow: "Flow" };
10632
- var PARTICLE_RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads" };
10909
+ var PARTICLE_KIND_LABELS = { string: "Strings", spring: "Springs", orbit: "Orbits", flow: "Flow", elastic: "Elastic", fuzzy: "Fuzzy" };
10910
+ var PARTICLE_RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads", ribbon: "Ribbon" };
10911
+ var RIBBON_PAIR_LABELS = { adjacent: "Neighbours", "first-last": "Outermost" };
10912
+ var DOT_TEXTURE_ORDER_LABELS = { sequence: "In turn", random: "Random" };
10913
+ var DOT_ROTATION_LABELS = { follow: "Follow motion", random: "Random", fixed: "Tip angle" };
10633
10914
  var STAMP_LABELS = { dot: "Dot", star: "Star", heart: "Heart", leaf: "Leaf", flower: "Flower", confetti: "Confetti", splat: "Splat", sparkle: "Sparkle", ring: "Ring" };
10915
+ function toggleStickerRef(refs, ref2) {
10916
+ if (refs.includes(ref2)) return refs.length > 1 ? refs.filter((r) => r !== ref2) : [...refs];
10917
+ return refs.length < MAX_STICKER_STAMPS ? [...refs, ref2] : [...refs];
10918
+ }
10919
+ function stickerRefLabel(ref2) {
10920
+ const stamp = ref2.startsWith("editor:") ? ref2.slice(7) : null;
10921
+ if (stamp && STAMP_LABELS[stamp]) return STAMP_LABELS[stamp];
10922
+ return ref2.slice(ref2.indexOf(":") + 1);
10923
+ }
10634
10924
  function toggleStamp(stamps, stamp) {
10635
10925
  const next = stamps.includes(stamp) ? stamps.filter((s) => s !== stamp) : [...stamps, stamp];
10636
10926
  return next.length ? next : [stamp];
10637
10927
  }
10928
+ function toggleDotTexture(textures, stamp) {
10929
+ if (textures.includes(stamp)) return textures.filter((s) => s !== stamp);
10930
+ return textures.length < MAX_DOT_TEXTURES ? [...textures, stamp] : [...textures];
10931
+ }
10638
10932
  var STROKE_SLIDERS = [
10639
10933
  { path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
10640
10934
  { path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
@@ -10957,6 +11251,10 @@ export {
10957
11251
  DEFAULT_VIEW,
10958
11252
  DEFAULT_WET_BRUSH,
10959
11253
  DEFAULT_WET_PARAMS,
11254
+ DOT_ROTATIONS,
11255
+ DOT_ROTATION_LABELS,
11256
+ DOT_TEXTURE_ORDERS,
11257
+ DOT_TEXTURE_ORDER_LABELS,
10960
11258
  DryStroke,
10961
11259
  EFFECT_MAP_REPEATS,
10962
11260
  EFFECT_STRIP_HEIGHT,
@@ -10974,8 +11272,10 @@ export {
10974
11272
  MAX_BRUSH_SIZE,
10975
11273
  MAX_CANVAS_SIDE,
10976
11274
  MAX_COORDINATE,
11275
+ MAX_DOT_TEXTURES,
10977
11276
  MAX_IMAGE_SIDE,
10978
11277
  MAX_LIVE_EFFECT_SOURCES,
11278
+ MAX_STICKER_STAMPS,
10979
11279
  MAX_ZOOM,
10980
11280
  MIN_CANVAS_SIDE,
10981
11281
  MIN_ZOOM,
@@ -11033,15 +11333,20 @@ export {
11033
11333
  PenDabWalker,
11034
11334
  PenSmoother,
11035
11335
  PerspectiveSnapper,
11336
+ RIBBON_PAIRS,
11337
+ RIBBON_PAIR_LABELS,
11036
11338
  SNAP_DECIDE_DISTANCE,
11037
11339
  STAMP_KINDS,
11038
11340
  STAMP_LABELS,
11039
11341
  STENCIL_KINDS,
11040
11342
  STENCIL_SHAPES,
11343
+ STICKER_ART_SIZE,
11041
11344
  STICKER_SLIDERS,
11042
11345
  STROKE_SLIDERS,
11043
11346
  SYMMETRY_MODES,
11044
11347
  StencilSnapper,
11348
+ StickerArtCache,
11349
+ StickerBrowser,
11045
11350
  StickerSpray,
11046
11351
  TRACING_NODE_ID,
11047
11352
  ThickStrokeDriver,
@@ -11061,6 +11366,7 @@ export {
11061
11366
  WET_TOOL_LABELS,
11062
11367
  WetStrokeDriver,
11063
11368
  addPaintGradientStop,
11369
+ addPolygonArea,
11064
11370
  affineCopy,
11065
11371
  applyAffine,
11066
11372
  backlightOf,
@@ -11088,6 +11394,7 @@ export {
11088
11394
  decodeImageBlob,
11089
11395
  defaultGuides,
11090
11396
  defaultImageEncoder,
11397
+ defaultStickerRasterizer,
11091
11398
  deserializePaintDocument,
11092
11399
  deserializePaintDocumentAsync,
11093
11400
  documentExtensions,
@@ -11206,6 +11513,7 @@ export {
11206
11513
  rgbToHexColor,
11207
11514
  rgbToHsl,
11208
11515
  rgbToHsv,
11516
+ sampleArt,
11209
11517
  sampleEffectAt,
11210
11518
  sampleEffectField,
11211
11519
  samplePressure,
@@ -11223,6 +11531,8 @@ export {
11223
11531
  stencilOutline,
11224
11532
  stickerAt,
11225
11533
  stickerRect,
11534
+ stickerRefLabel,
11535
+ stickerStamp,
11226
11536
  stickerSurface,
11227
11537
  strokeBounds,
11228
11538
  strokeDuration,
@@ -11238,7 +11548,9 @@ export {
11238
11548
  timeLapseVideo,
11239
11549
  timeLapseZip,
11240
11550
  toStrokePoint,
11551
+ toggleDotTexture,
11241
11552
  toggleStamp,
11553
+ toggleStickerRef,
11242
11554
  translateStencil,
11243
11555
  updatePaintGradientStop,
11244
11556
  usesPaintExchange,