@pixerate/editor 0.257.0 → 0.258.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.
@@ -3055,6 +3055,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
3055
3055
  if (filter) points = points.map(filter);
3056
3056
  for (const p of points) this.points.push(p);
3057
3057
  this.lastRaw = points[points.length - 1];
3058
+ this.host.sampled?.(this, points);
3058
3059
  this.lightTwin?.push(points);
3059
3060
  if (this.wetDriver) {
3060
3061
  this.wetUpdate(this.wetDriver.push(points));
@@ -5854,6 +5855,7 @@ var PaintEditorController = class _PaintEditorController {
5854
5855
  encodeImage;
5855
5856
  listeners = /* @__PURE__ */ new Set();
5856
5857
  commitListeners = /* @__PURE__ */ new Set();
5858
+ sampleListeners = /* @__PURE__ */ new Set();
5857
5859
  /** Origins set by `withOrigin`, innermost last. */
5858
5860
  origins = [];
5859
5861
  /** `group` in progress: commits fold into one step, recorded when it ends. */
@@ -5968,6 +5970,27 @@ var PaintEditorController = class _PaintEditorController {
5968
5970
  this.listeners.add(listener);
5969
5971
  return () => this.listeners.delete(listener);
5970
5972
  }
5973
+ /**
5974
+ * Hears the user's live strokes as they are painted (`beginStroke` sessions, not strokes painted
5975
+ * from data): when the pen goes down, each batch of samples, and the end or cancel. For agents
5976
+ * that paint along in real time. Returns an unsubscribe function.
5977
+ */
5978
+ onStrokeSample(listener) {
5979
+ this.sampleListeners.add(listener);
5980
+ return () => {
5981
+ this.sampleListeners.delete(listener);
5982
+ };
5983
+ }
5984
+ notifySample(phase, session, added = [], strokeId) {
5985
+ if (!this.sampleListeners.size || this.previewing) return;
5986
+ const sample = { phase, layerId: session.layerId, brush: session.init.brush, color: session.init.colorHex, points: session.points, added, ...strokeId !== void 0 ? { strokeId } : {} };
5987
+ for (const l of [...this.sampleListeners]) l(sample);
5988
+ }
5989
+ /** The stroke being painted now, or `null`: its layer, brush, colour and samples so far. */
5990
+ getLiveStroke() {
5991
+ const s = this.session;
5992
+ return s ? { layerId: s.layerId, brushId: s.init.brush.id, brushName: s.init.brush.name, color: s.init.colorHex, points: s.points } : null;
5993
+ }
5971
5994
  /** Hears each undo step recorded, merged, undone or redone, with who made it. Returns an unsubscribe function. */
5972
5995
  onCommit(listener) {
5973
5996
  this.commitListeners.add(listener);
@@ -7700,6 +7723,7 @@ var PaintEditorController = class _PaintEditorController {
7700
7723
  this.liveThick = ch ? { layerId, channels: ch } : null;
7701
7724
  this.emit(rects);
7702
7725
  },
7726
+ sampled: (s, points) => this.notifySample("points", s, points),
7703
7727
  commit: (s, surface, cancelled) => {
7704
7728
  this.session = null;
7705
7729
  this.live = null;
@@ -7719,6 +7743,7 @@ var PaintEditorController = class _PaintEditorController {
7719
7743
  this.wet.syncDisplays();
7720
7744
  }
7721
7745
  this.emit();
7746
+ this.notifySample(cancelled ? "cancel" : "end", s, [], cancelled ? void 0 : null);
7722
7747
  return;
7723
7748
  }
7724
7749
  const stroke = this.record(s);
@@ -7738,6 +7763,7 @@ var PaintEditorController = class _PaintEditorController {
7738
7763
  const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
7739
7764
  const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
7740
7765
  this.commit(`${brush.name} stroke`, this.withWetEvents(withLiveStroke({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {}, ...light ? { light } : {} }, stroke)), wetStroke ? before : this.doc);
7766
+ this.notifySample("end", s, [], stroke.id);
7741
7767
  }
7742
7768
  };
7743
7769
  let session;
@@ -7749,6 +7775,7 @@ var PaintEditorController = class _PaintEditorController {
7749
7775
  }
7750
7776
  this.session = session;
7751
7777
  this.emit([]);
7778
+ this.notifySample("begin", session);
7752
7779
  return session;
7753
7780
  }
7754
7781
  /** The current live stroke, if any. */
@@ -11536,6 +11563,23 @@ function createPaintEditorToolProvider(controller, options = {}) {
11536
11563
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
11537
11564
  }
11538
11565
  });
11566
+ add({
11567
+ name: "get_live_stroke",
11568
+ title: "Get the stroke being painted",
11569
+ description: "Reads the stroke the user is painting right now, if any: its layer, brush id and name, colour, and the pen samples so far (document pixels; at most the last `limit`). Returns live: false when no stroke is in progress. For agents that paint along with the user: hosts stream the same samples to them live (onStrokeSample); this is a snapshot.",
11570
+ inputSchema: {
11571
+ type: "object",
11572
+ properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
11573
+ additionalProperties: false
11574
+ },
11575
+ annotations: { title: "Get the stroke being painted", ...READ6 },
11576
+ handler: (args) => {
11577
+ const live = c.getLiveStroke();
11578
+ if (!live) return { live: false };
11579
+ const limit = args.limit ?? 500;
11580
+ return { live: true, layerId: live.layerId, brushId: live.brushId, brushName: live.brushName, color: live.color, total: live.points.length, points: live.points.slice(-limit) };
11581
+ }
11582
+ });
11539
11583
  add({
11540
11584
  name: "list_strokes",
11541
11585
  title: "List recorded strokes",
@@ -12967,6 +13011,37 @@ function paintProposalGhost(proposal) {
12967
13011
  if (!b || !b.width || !b.height || proposal.ghost.length !== b.width * b.height * 4) return null;
12968
13012
  return { x: b.x, y: b.y, width: b.width, height: b.height, data: proposal.ghost };
12969
13013
  }
13014
+ function createPaintAgentStrokes() {
13015
+ let state = { strokes: [] };
13016
+ const listeners = /* @__PURE__ */ new Set();
13017
+ const set = (strokes) => {
13018
+ state = { strokes };
13019
+ listeners.forEach((l) => l(state));
13020
+ };
13021
+ return {
13022
+ getState: () => state,
13023
+ subscribe(listener) {
13024
+ listeners.add(listener);
13025
+ return () => {
13026
+ listeners.delete(listener);
13027
+ };
13028
+ },
13029
+ set(stroke) {
13030
+ const i = state.strokes.findIndex((s) => s.id === stroke.id);
13031
+ set(i < 0 ? [...state.strokes, stroke] : state.strokes.map((s, j) => j === i ? stroke : s));
13032
+ },
13033
+ remove(id) {
13034
+ if (state.strokes.some((s) => s.id === id)) set(state.strokes.filter((s) => s.id !== id));
13035
+ },
13036
+ clear(origin) {
13037
+ const keep = origin ? state.strokes.filter((s) => s.origin !== origin) : [];
13038
+ if (keep.length !== state.strokes.length) set(keep);
13039
+ }
13040
+ };
13041
+ }
13042
+ function paintAgentStrokePoints(stroke) {
13043
+ return stroke.points.map((p) => `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}`).join(" ");
13044
+ }
12970
13045
 
12971
13046
  // src/paint-editor/collaboration-tools.ts
12972
13047
  var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
@@ -13256,6 +13331,7 @@ export {
13256
13331
  colorHarmony2 as colorHarmony,
13257
13332
  composeAffine,
13258
13333
  contactSheet,
13334
+ createPaintAgentStrokes,
13259
13335
  createPaintBrushLibraryToolProvider,
13260
13336
  createPaintChangeFeed,
13261
13337
  createPaintCompositor,
@@ -13348,6 +13424,7 @@ export {
13348
13424
  normalizeWetParams,
13349
13425
  overPixels,
13350
13426
  padSampleRadius,
13427
+ paintAgentStrokePoints,
13351
13428
  paintGradientMappingFor,
13352
13429
  paintPlaybackPanel,
13353
13430
  paintProposalGhost,
@@ -26,6 +26,8 @@ export interface StrokeSessionHost {
26
26
  update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;
27
27
  /** Ends the stroke: `surface` is the new pixels, or `null` when nothing changed / it was cancelled. */
28
28
  commit(session: PaintStrokeSession, surface: TiledSurface | null, cancelled: boolean): void;
29
+ /** Hears the raw samples as they arrive (after the input filter), before they are painted. */
30
+ sampled?(session: PaintStrokeSession, points: readonly PenPoint[]): void;
29
31
  }
30
32
  export interface StrokeSessionInit {
31
33
  layerId: string;
@@ -26,6 +26,8 @@ export interface StrokeSessionHost {
26
26
  update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;
27
27
  /** Ends the stroke: `surface` is the new pixels, or `null` when nothing changed / it was cancelled. */
28
28
  commit(session: PaintStrokeSession, surface: TiledSurface | null, cancelled: boolean): void;
29
+ /** Hears the raw samples as they arrive (after the input filter), before they are painted. */
30
+ sampled?(session: PaintStrokeSession, points: readonly PenPoint[]): void;
29
31
  }
30
32
  export interface StrokeSessionInit {
31
33
  layerId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.257.0",
3
+ "version": "0.258.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -321,6 +321,7 @@ Listing, saving, sharing and previewing brushes are the brush library's tools (`
321
321
  | --- | --- |
322
322
  | `paint_stroke` | Paint one stroke through `points` (document px; `pressure` 0..1, `t` ms, `tiltX` / `tiltY` or `altitude` / `azimuth`, `twist`) with `brushId` (default the active brush), optional per-stroke `brush` facet changes (as `set_brush` takes), `color` or `paint` (a gradient for this stroke), `layerId`, `seed`, `snapToPerspective`. The symmetry guide adds its copies; a sampling tracing image supplies the colour when `color` is missing. Returns `layerId`, `bounds`, `dabs`, `strokeId` and, for brushes that pick up paint, `mixerLoad` (what the brush carries now). Refused on locked, hidden or group layers, with the fix in the message. |
323
323
  | `list_strokes` | Read the recorded strokes, oldest first (default the last 100): id, layer, brush, colour, point count, seed, bounds, `live`. With `ids`, their points too; with `layerId`, one layer's. With `live: true`, the editable strokes of stroke layers as they are now, bottom first: the ids for the stroke layer tools. |
324
+ | `get_live_stroke` | Read the stroke the user is painting right now (layer, brush, colour, samples so far; `live: false` when none). For painting along with the user; hosts stream the samples live. |
324
325
  | `stroke_at` | Read the top-most stroke of a stroke layer that painted the pixel at `x`, `y`, or null. Use it to pick one stroke among overlapping ones. |
325
326
  | `update_stroke` | Change a stroke on a stroke layer: `brushId` (keeps its size), `brush` facets (as `set_brush` takes), `color`, `paint` (null drops a gradient), `seed`. Redraws exactly. One undo step. Returns the stroke and the changed `bounds`. |
326
327
  | `edit_stroke_points` | Replace a stroke's `points`, or `transform` it (`dx`, `dy`, `scale`, `rotate`, `flipX`, about its centre or `origin`). One undo step. Returns the stroke with its points and the changed `bounds`. |