@pixerate/editor-svelte 0.101.4 → 0.103.1

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 (33) hide show
  1. package/dist/agentTools.svelte.d.ts +4 -2
  2. package/dist/agentTools.svelte.js +8 -1
  3. package/dist/animation-editor/AnimationEditor.svelte +35 -3
  4. package/dist/animation-editor/AnimationEditor.svelte.d.ts +10 -0
  5. package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +30 -0
  6. package/dist/canvas/createCanvasPlaybackSurface.svelte.js +43 -0
  7. package/dist/canvas/index.d.ts +2 -0
  8. package/dist/canvas/index.js +4 -1
  9. package/dist/chart/ChartEditor.svelte +85 -4
  10. package/dist/chart/ChartEditor.svelte.d.ts +11 -0
  11. package/dist/image-editor/ImageEditor.svelte +91 -1
  12. package/dist/image-editor/ImageEditor.svelte.d.ts +10 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/paint-editor/PaintEditor.svelte +92 -10
  16. package/dist/paint-editor/PaintEditor.svelte.d.ts +12 -1
  17. package/dist/parameters/ParametersPanel.svelte +31 -2
  18. package/dist/parameters/ParametersPanel.svelte.d.ts +10 -0
  19. package/dist/playback/PresenceLayer.svelte +65 -0
  20. package/dist/playback/PresenceLayer.svelte.d.ts +22 -0
  21. package/dist/playback/index.d.ts +2 -0
  22. package/dist/playback/index.js +2 -0
  23. package/dist/playback/playback.svelte.d.ts +32 -0
  24. package/dist/playback/playback.svelte.js +91 -0
  25. package/dist/spreadsheet/SpreadsheetEditor.svelte +63 -3
  26. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +10 -0
  27. package/dist/vector/VectorCanvas.svelte +54 -1
  28. package/dist/vector/VectorCanvas.svelte.d.ts +10 -0
  29. package/dist/video-editor/VideoEditor.svelte +8 -3
  30. package/dist/video-editor/VideoEditor.svelte.d.ts +5 -0
  31. package/dist/video-editor/VideoTimeline.svelte +64 -2
  32. package/dist/video-editor/VideoTimeline.svelte.d.ts +10 -0
  33. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import type { AgentToolProvider, ToolRegistry } from "@pixerate/editor/tools";
1
+ import type { AgentPlayback, AgentToolProvider, ToolRegistry } from "@pixerate/editor/tools";
2
2
  type MaybeGetter<T> = T | (() => T);
3
3
  /**
4
4
  * Registers a tool provider with a `ToolRegistry` for the lifetime of the
@@ -7,5 +7,7 @@ type MaybeGetter<T> = T | (() => T);
7
7
  * `useAgentToolProvider(registry, () => editor && createRichTextToolProvider(editor, { id }))`:
8
8
  * when the getter returns a different provider, the old one is unregistered.
9
9
  */
10
- export declare function useAgentToolProvider(registry: MaybeGetter<ToolRegistry | null | undefined>, provider: MaybeGetter<AgentToolProvider | null | undefined>): void;
10
+ export declare function useAgentToolProvider(registry: MaybeGetter<ToolRegistry | null | undefined>, provider: MaybeGetter<AgentToolProvider | null | undefined>, options?: {
11
+ playback?: MaybeGetter<AgentPlayback | null | undefined>;
12
+ }): void;
11
13
  export {};
@@ -6,7 +6,14 @@ const read = (value) => typeof value === "function" ? value() : value;
6
6
  * `useAgentToolProvider(registry, () => editor && createRichTextToolProvider(editor, { id }))`:
7
7
  * when the getter returns a different provider, the old one is unregistered.
8
8
  */
9
- export function useAgentToolProvider(registry, provider) {
9
+ export function useAgentToolProvider(registry, provider, options = {}) {
10
+ $effect(() => {
11
+ const currentRegistry = read(registry);
12
+ const playback = options.playback === undefined ? undefined : read(options.playback);
13
+ // Plays the registry's calls with this playback (installed once per registry).
14
+ if (currentRegistry && playback)
15
+ playback.install(currentRegistry);
16
+ });
10
17
  $effect(() => {
11
18
  const currentRegistry = read(registry);
12
19
  const currentProvider = read(provider);
@@ -10,6 +10,9 @@
10
10
  import type { EffectProvider } from '@pixerate/editor/effects';
11
11
  import { useEffects } from '../effects/effects.svelte.js';
12
12
  import AnimationEffectsPanel from './AnimationEffectsPanel.svelte';
13
+ import type { AgentPlayback } from '@pixerate/editor/tools';
14
+ import { createElementPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
15
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
13
16
 
14
17
  interface Props {
15
18
  controller: AnimationEditorController;
@@ -33,9 +36,32 @@
33
36
  */
34
37
  effects?: EffectProvider | null;
35
38
  class?: string;
39
+ /**
40
+ * Agent playback: act out the animation tools' calls here with the agent's cursor (default:
41
+ * `setAgentPlayback` above; `null` turns it off).
42
+ */
43
+ playback?: AgentPlayback | null;
44
+ /** The playback surface id (default `animation`, where the animation tool providers play). */
45
+ playbackId?: string;
46
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
47
+ onplaybackreveal?: () => void | Promise<void>;
36
48
  }
37
49
 
38
- let { controller, values, onionSkin, motionPaths, showIssues = true, editable = true, effects, class: className = '' }: Props = $props();
50
+ let { controller, values, onionSkin, motionPaths, showIssues = true, editable = true, effects, class: className = '', playback, playbackId = 'animation', onplaybackreveal }: Props = $props();
51
+
52
+ // Agent playback: the cursor clicks shots in the timeline and visits elements on the stage
53
+ // (whose SVG keeps the artwork's ids, prefixed `ae-`).
54
+ let root: HTMLDivElement | undefined = $state();
55
+ const ambientPlayback = getAgentPlayback();
56
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
57
+ const agentPresence = createElementPlaybackSurface(() => playing, () => playbackId, () => root, () => ({
58
+ defaultKind: 'shot',
59
+ selectors: {
60
+ shot: (id: string) => `[data-shot="${CSS.escape(id)}"]`,
61
+ element: (id: string) => `[data-animation-stage] [id="ae-${CSS.escape(id)}"]`
62
+ },
63
+ reveal: onplaybackreveal
64
+ }));
39
65
  useEffects(() => controller, () => effects);
40
66
  const view = animationEditorStateOf(() => controller);
41
67
  const editor = $derived(view.current);
@@ -84,14 +110,20 @@
84
110
  scene settings by hand. Every edit is one undo step.
85
111
  -->
86
112
  {#if !editable}
87
- <div class="flex flex-col gap-2 {className}" data-animation-editor="">
113
+ <div bind:this={root} class="relative flex flex-col gap-2 {className}" data-animation-editor="">
114
+ {#if playing}
115
+ <PresenceLayer presence={agentPresence} />
116
+ {/if}
88
117
  <AnimationStage {controller} {values} onionSkin={onionSkin} {motionPaths} class="rounded-md border border-border bg-muted/40" />
89
118
  <PlayheadScrubber {controller} />
90
119
  {@render issueList()}
91
120
  </div>
92
121
  {:else}
93
122
  <!-- svelte-ignore a11y_no_static_element_interactions -->
94
- <div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] {className}" data-animation-editor="" {onkeydown}>
123
+ <div bind:this={root} class="relative grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] {className}" data-animation-editor="" {onkeydown}>
124
+ {#if playing}
125
+ <PresenceLayer presence={agentPresence} />
126
+ {/if}
95
127
  <div class="flex min-w-0 flex-col gap-2">
96
128
  <AnimationStage
97
129
  {controller}
@@ -1,5 +1,6 @@
1
1
  import { type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin } from '@pixerate/editor/animation-editor';
2
2
  import type { EffectProvider } from '@pixerate/editor/effects';
3
+ import type { AgentPlayback } from '@pixerate/editor/tools';
3
4
  interface Props {
4
5
  controller: AnimationEditorController;
5
6
  values?: MotionFrameOptions['values'];
@@ -22,6 +23,15 @@ interface Props {
22
23
  */
23
24
  effects?: EffectProvider | null;
24
25
  class?: string;
26
+ /**
27
+ * Agent playback: act out the animation tools' calls here with the agent's cursor (default:
28
+ * `setAgentPlayback` above; `null` turns it off).
29
+ */
30
+ playback?: AgentPlayback | null;
31
+ /** The playback surface id (default `animation`, where the animation tool providers play). */
32
+ playbackId?: string;
33
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
34
+ onplaybackreveal?: () => void | Promise<void>;
25
35
  }
26
36
  declare const AnimationEditor: import("svelte").Component<Props, {}, "">;
27
37
  type AnimationEditor = ReturnType<typeof AnimationEditor>;
@@ -0,0 +1,30 @@
1
+ import { type CanvasEdge, type CanvasNode, type NodeRectOptions } from '@pixerate/editor/canvas';
2
+ import type { AgentPlayback, PresenceStore } from '@pixerate/editor/tools';
3
+ type MaybeGetter<T> = T | (() => T);
4
+ export interface CreateCanvasPlaybackSurfaceOptions<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge> {
5
+ /** The flow's nodes and edges now (e.g. the `bind:nodes` state). */
6
+ getNodes: () => readonly TNode[];
7
+ getEdges?: () => readonly TEdge[];
8
+ /** The flow viewport now (Svelte Flow's `useSvelteFlow().getViewport`). */
9
+ getViewport: () => {
10
+ x: number;
11
+ y: number;
12
+ zoom: number;
13
+ };
14
+ /** The element the flow fills (and the `<PresenceLayer>` sits in); for bounds and screen mapping. */
15
+ container: () => HTMLElement | null | undefined;
16
+ /** Node size resolution (match the canvas' own). */
17
+ rectOptions?: NodeRectOptions;
18
+ /** Bring the canvas on screen (switch to its tab) before the agent acts. */
19
+ reveal?: () => void | Promise<void>;
20
+ /** Whether the canvas is on screen (default: the container has a size and the page is visible). */
21
+ visible?: () => boolean;
22
+ }
23
+ /**
24
+ * Svelte 5 rune: attaches the node canvas as an agent playback surface under `id` (the canvas tool
25
+ * provider's id) for the component's lifetime (call during init) and returns its cursors. Draw them
26
+ * with `<PresenceLayer presence={presence} viewport={viewport} />` inside the container. Parity with
27
+ * React's `useCanvasPlaybackSurface`.
28
+ */
29
+ export declare function createCanvasPlaybackSurface<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge>(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options: CreateCanvasPlaybackSurfaceOptions<TNode, TEdge>): PresenceStore;
30
+ export {};
@@ -0,0 +1,43 @@
1
+ import { createCanvasPlaybackLocator } from '@pixerate/editor/canvas';
2
+ import { createPlaybackSurface } from '../playback/playback.svelte.js';
3
+ /**
4
+ * Svelte 5 rune: attaches the node canvas as an agent playback surface under `id` (the canvas tool
5
+ * provider's id) for the component's lifetime (call during init) and returns its cursors. Draw them
6
+ * with `<PresenceLayer presence={presence} viewport={viewport} />` inside the container. Parity with
7
+ * React's `useCanvasPlaybackSurface`.
8
+ */
9
+ export function createCanvasPlaybackSurface(playback, id, options) {
10
+ const locate = createCanvasPlaybackLocator({
11
+ getNodes: options.getNodes,
12
+ getEdges: () => options.getEdges?.() ?? [],
13
+ rectOptions: options.rectOptions
14
+ });
15
+ const box = () => options.container()?.getBoundingClientRect() ?? null;
16
+ return createPlaybackSurface(playback, id, {
17
+ defaultKind: 'node',
18
+ locate,
19
+ bounds: () => {
20
+ const r = box();
21
+ const { x, y, zoom } = options.getViewport();
22
+ return r && r.width > 0 ? { x: -x / zoom, y: -y / zoom, width: r.width / zoom, height: r.height / zoom } : null;
23
+ },
24
+ visible: () => {
25
+ if (options.visible)
26
+ return options.visible();
27
+ const r = box();
28
+ const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden';
29
+ return !!r && r.width > 0 && r.height > 0 && !hidden;
30
+ },
31
+ toViewport: (p) => {
32
+ const r = box();
33
+ const { x, y, zoom } = options.getViewport();
34
+ return { x: (r?.left ?? 0) + p.x * zoom + x, y: (r?.top ?? 0) + p.y * zoom + y };
35
+ },
36
+ fromViewport: (p) => {
37
+ const r = box();
38
+ const { x, y, zoom } = options.getViewport();
39
+ return { x: (p.x - (r?.left ?? 0) - x) / zoom, y: (p.y - (r?.top ?? 0) - y) / zoom };
40
+ },
41
+ reveal: () => options.reveal?.()
42
+ });
43
+ }
@@ -18,6 +18,8 @@ export { createStreamingCanvas, type CreateStreamingCanvasOptions } from './crea
18
18
  export { createCanvasArrange, createCanvasArrangeShortcuts, type CanvasArrange, type CreateCanvasArrangeOptions, type CanvasArrangeShortcutsOptions } from './createCanvasArrange.svelte.js';
19
19
  export { createCanvasScenes, type CanvasScenes, type CreateCanvasScenesOptions } from './createCanvasScenes.svelte.js';
20
20
  export { createCanvasCapture, type CreateCanvasCaptureOptions } from './createCanvasCapture.js';
21
+ export { createCanvasPlaybackSurface, type CreateCanvasPlaybackSurfaceOptions } from './createCanvasPlaybackSurface.svelte.js';
22
+ export { default as PresenceLayer } from '../playback/PresenceLayer.svelte';
21
23
  export { default as CanvasGuidesLayer } from './CanvasGuidesLayer.svelte';
22
24
  export { createCanvasDraw, type CanvasDraw, type CreateCanvasDrawOptions } from './createCanvasDraw.svelte.js';
23
25
  export { default as CanvasDrawLayer } from './CanvasDrawLayer.svelte';
@@ -26,7 +26,8 @@ export { createCanvasMultiDrag } from './createCanvasMultiDrag.svelte.js';
26
26
  export { getMarqueeSelectionPreset, handleDeselectOnEscape } from './createCanvasSelection.svelte.js';
27
27
  // Viewport floating scrollbar component
28
28
  export { default as FloatingHorizontalScrollbar } from './FloatingHorizontalScrollbar.svelte';
29
- // Agent presence visual layer
29
+ // Agent presence visual layer. Deprecated: use `PresenceLayer` with `createCanvasPlaybackSurface`
30
+ // (agent playback), which plays every canvas tool call.
30
31
  export { default as AgentPresenceLayer } from './AgentPresenceLayer.svelte';
31
32
  export { default as AgentCursorPresence } from './AgentCursorPresence.svelte';
32
33
  // Streaming canvas rune
@@ -35,6 +36,8 @@ export { createStreamingCanvas } from './createStreamingCanvas.svelte.js';
35
36
  export { createCanvasArrange, createCanvasArrangeShortcuts } from './createCanvasArrange.svelte.js';
36
37
  export { createCanvasScenes } from './createCanvasScenes.svelte.js';
37
38
  export { createCanvasCapture } from './createCanvasCapture.js';
39
+ export { createCanvasPlaybackSurface } from './createCanvasPlaybackSurface.svelte.js';
40
+ export { default as PresenceLayer } from '../playback/PresenceLayer.svelte';
38
41
  export { default as CanvasGuidesLayer } from './CanvasGuidesLayer.svelte';
39
42
  // Freehand drawing tools (pen, highlighter, eraser)
40
43
  export { createCanvasDraw } from './createCanvasDraw.svelte.js';
@@ -1,6 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { ComponentProps } from 'svelte';
3
- import { describeChartSelection, downloadChartPng, type ChartController, type DownloadChartPngOptions } from '@pixerate/editor/chart';
3
+ import { describeChartSelection, downloadChartPng, locateChartPoint, ChartPresence, type ChartController, type ChartScene, type DownloadChartPngOptions } from '@pixerate/editor/chart';
4
+ import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
5
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
6
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
4
7
  import ChartCanvas from './ChartCanvas.svelte';
5
8
  import ChartDataGrid from './ChartDataGrid.svelte';
6
9
  import ChartInspector from './ChartInspector.svelte';
@@ -21,9 +24,83 @@
21
24
  height?: number;
22
25
  canvasProps?: Omit<ComponentProps<typeof ChartCanvas>, 'controller' | 'height'>;
23
26
  class?: string;
27
+ /**
28
+ * Agent playback: act out the chart tools' calls here with the agent's cursor, on the inspector,
29
+ * the data grid and the toolbar (default: `setAgentPlayback` above; `null` turns it off). With
30
+ * `canvasProps.presence` (a `ChartDirector`'s), the director's drags share the cursor.
31
+ */
32
+ playback?: AgentPlayback | null;
33
+ /** The playback surface id (default `chart`, where the chart tool provider plays). */
34
+ playbackId?: string;
35
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
36
+ onplaybackreveal?: () => void | Promise<void>;
24
37
  }
25
38
 
26
- let { controller, showInspector = true, showDataGrid = true, showToolbar = true, pngOptions, height = 380, canvasProps = {}, class: className = '' }: Props = $props();
39
+ let { controller, showInspector = true, showDataGrid = true, showToolbar = true, pngOptions, height = 380, canvasProps = {}, class: className = '', playback, playbackId = 'chart', onplaybackreveal }: Props = $props();
40
+
41
+ // Agent playback in the chart's scene coordinates, so a director's drags (in the same presence
42
+ // store) and the editor's own controls share one cursor. Controls outside the chart are mapped
43
+ // into scene units through the SVG's box.
44
+ let root: HTMLDivElement | undefined = $state();
45
+ const ownScene: { current: ChartScene | null } = { current: null };
46
+ const sceneRef = $derived(canvasProps.sceneRef ?? ownScene);
47
+ const ambientPlayback = getAgentPlayback();
48
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
49
+ const svgBox = () => {
50
+ const svg = root?.querySelector<SVGSVGElement>('[data-chart-canvas] svg');
51
+ const r = svg?.getBoundingClientRect();
52
+ const vb = svg?.viewBox?.baseVal;
53
+ if (!svg || !r || !vb || !vb.width || !vb.height) return null;
54
+ return { left: r.left, top: r.top, sx: r.width / vb.width, sy: r.height / vb.height, vx: vb.x, vy: vb.y };
55
+ };
56
+ const sceneToClient = (p: PlaybackPoint): PlaybackPoint | null => {
57
+ const b = svgBox();
58
+ return b ? { x: b.left + (p.x - b.vx) * b.sx, y: b.top + (p.y - b.vy) * b.sy } : null;
59
+ };
60
+ const clientToScene = (p: PlaybackPoint): PlaybackPoint | null => {
61
+ const b = svgBox();
62
+ return b && b.sx && b.sy ? { x: (p.x - b.left) / b.sx + b.vx, y: (p.y - b.top) / b.sy + b.vy } : null;
63
+ };
64
+ const elementRect = (selector: string): PlaybackRect | null => {
65
+ const el = root?.querySelector(selector);
66
+ // The inspector scrolls on its own: bring the control into its view first.
67
+ el?.scrollIntoView?.({ block: 'nearest' });
68
+ const r = el?.getBoundingClientRect();
69
+ if (!r || r.width <= 0) return null;
70
+ const a = clientToScene({ x: r.left, y: r.top });
71
+ const b = clientToScene({ x: r.right, y: r.bottom });
72
+ return a && b ? { x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y } : null;
73
+ };
74
+ const SELECTORS: Record<string, (id: string) => string> = {
75
+ control: (id) => `[data-option="${CSS.escape(id)}"]`,
76
+ 'chart-type': (id) => `[data-chart-type="${CSS.escape(id)}"]`,
77
+ action: (id) => `[data-action="${CSS.escape(id)}"]`,
78
+ grid: () => '[data-chart-grid]'
79
+ };
80
+ // svelte-ignore state_referenced_locally
81
+ const ownPresence = canvasProps.presence ?? new ChartPresence();
82
+ const presence = createPlaybackSurface(() => playing, () => playbackId, () => ({
83
+ defaultKind: 'point',
84
+ locate: (ref: PlaybackRef) => {
85
+ if (ref.kind === 'point') return sceneRef.current ? locateChartPoint(controller.getSpec(), sceneRef.current, ref) : null;
86
+ const selector = SELECTORS[ref.kind]?.(ref.id);
87
+ return selector ? elementRect(selector) : null;
88
+ },
89
+ bounds: () => (sceneRef.current ? { x: 0, y: 0, width: sceneRef.current.width, height: sceneRef.current.height } : null),
90
+ visible: () => {
91
+ const r = root?.getBoundingClientRect();
92
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
93
+ },
94
+ toViewport: (p: PlaybackPoint) => sceneToClient(p) ?? p,
95
+ fromViewport: (p: PlaybackPoint) => clientToScene(p) ?? p,
96
+ reveal: () => onplaybackreveal?.()
97
+ }), ownPresence);
98
+ /** Scene units → pixels in the editor's root, for the cursor layer. */
99
+ const sceneToRoot = (p: PlaybackPoint): PlaybackPoint => {
100
+ const c = sceneToClient(p);
101
+ const box = root?.getBoundingClientRect();
102
+ return c && box ? { x: c.x - box.left, y: c.y - box.top } : p;
103
+ };
27
104
 
28
105
  const view = chartStateOf(() => controller);
29
106
  const chart = $derived(view.current);
@@ -34,7 +111,10 @@
34
111
  }
35
112
  </script>
36
113
 
37
- <div class="flex flex-col gap-2 {className}" data-chart-editor="">
114
+ <div bind:this={root} class="relative flex flex-col gap-2 {className}" data-chart-editor="">
115
+ {#if playing}
116
+ <PresenceLayer {presence} viewport={sceneToRoot} />
117
+ {/if}
38
118
  {#if showToolbar}
39
119
  <div class="flex items-center gap-1" data-chart-toolbar="">
40
120
  <button type="button" class={chartButtonClass} disabled={!chart.canUndo} title={chart.undoLabel ? `Undo ${chart.undoLabel}` : 'Undo'} data-action="undo" onclick={() => controller.undo()}>Undo</button>
@@ -53,7 +133,8 @@
53
133
  {/if}
54
134
  <div class="flex min-h-0 flex-col gap-2 md:flex-row">
55
135
  <div class="min-w-0 flex-1 rounded-md border border-border bg-background p-1">
56
- <ChartCanvas {controller} {height} {...canvasProps} />
136
+ <!-- With playback the editor draws the cursor (over the inspector too), so the canvas doesn't. -->
137
+ <ChartCanvas {controller} {height} {...canvasProps} {sceneRef} presence={playing ? undefined : canvasProps.presence} />
57
138
  </div>
58
139
  {#if showInspector}
59
140
  <!-- Side by side, the canvas sets the row's height and the inspector scrolls within it. -->
@@ -1,5 +1,6 @@
1
1
  import type { ComponentProps } from 'svelte';
2
2
  import { type ChartController, type DownloadChartPngOptions } from '@pixerate/editor/chart';
3
+ import type { AgentPlayback } from '@pixerate/editor/tools';
3
4
  import ChartCanvas from './ChartCanvas.svelte';
4
5
  interface Props {
5
6
  controller: ChartController;
@@ -15,6 +16,16 @@ interface Props {
15
16
  height?: number;
16
17
  canvasProps?: Omit<ComponentProps<typeof ChartCanvas>, 'controller' | 'height'>;
17
18
  class?: string;
19
+ /**
20
+ * Agent playback: act out the chart tools' calls here with the agent's cursor, on the inspector,
21
+ * the data grid and the toolbar (default: `setAgentPlayback` above; `null` turns it off). With
22
+ * `canvasProps.presence` (a `ChartDirector`'s), the director's drags share the cursor.
23
+ */
24
+ playback?: AgentPlayback | null;
25
+ /** The playback surface id (default `chart`, where the chart tool provider plays). */
26
+ playbackId?: string;
27
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
28
+ onplaybackreveal?: () => void | Promise<void>;
18
29
  }
19
30
  declare const ChartEditor: import("svelte").Component<Props, {}, "">;
20
31
  type ChartEditor = ReturnType<typeof ChartEditor>;
@@ -11,8 +11,13 @@
11
11
  ImageEditorAIHooks,
12
12
  ImageEditorFit,
13
13
  } from '@pixerate/editor/image-editor';
14
+ import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
15
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
16
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
14
17
  import {
15
18
  canvasToImagePoint,
19
+ imageToCanvasPoint,
20
+ createImagePlaybackLocator,
16
21
  hitTestCrop,
17
22
  getCropCursor,
18
23
  calculateCropDrag,
@@ -71,6 +76,15 @@
71
76
  * with an Effects section in the Layers tab. Optional: without one the editor is as before.
72
77
  */
73
78
  effects?: EffectProvider | null;
79
+ /**
80
+ * Agent playback: act out the image tools' calls here with the agent's cursor, opening the
81
+ * panel each tool belongs to (default: `setAgentPlayback` above; `null` turns it off).
82
+ */
83
+ playback?: AgentPlayback | null;
84
+ /** The playback surface id (default `image`, where the image tool providers play). */
85
+ playbackId?: string;
86
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
87
+ onplaybackreveal?: () => void | Promise<void>;
74
88
  }
75
89
 
76
90
  let {
@@ -85,6 +99,9 @@
85
99
  aiHooks,
86
100
  fit,
87
101
  effects,
102
+ playback,
103
+ playbackId = 'image',
104
+ onplaybackreveal,
88
105
  }: Props = $props();
89
106
 
90
107
  const internalEditor = createReactiveImageEditor();
@@ -101,6 +118,67 @@
101
118
  let canvasEl = $state<HTMLCanvasElement | null>(null);
102
119
  let containerEl = $state<HTMLDivElement | null>(null);
103
120
  let activeTab = $state<'crop' | 'adjust' | 'layers' | 'paint' | 'select' | 'annotate' | 'depth' | 'ai'>('crop');
121
+ let rootEl = $state<HTMLDivElement | null>(null);
122
+
123
+ // Agent playback: the agent's cursor in document pixels, over the whole editor (canvas and panels).
124
+ const ambientPlayback = getAgentPlayback();
125
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
126
+ /** Document pixels → pixels in the editor's root element, through the canvas projection. */
127
+ const docToRoot = (p: PlaybackPoint): PlaybackPoint => {
128
+ if (!canvasEl || !rootEl || !canvasEl.width) return p;
129
+ const r = canvasEl.getBoundingClientRect();
130
+ const box = rootEl.getBoundingClientRect();
131
+ const c = imageToCanvasPoint(p, editor.controller.getState(), canvasEl.width, canvasEl.height, activeTab === 'crop');
132
+ return { x: r.left - box.left + (c.x * r.width) / canvasEl.width, y: r.top - box.top + (c.y * r.height) / canvasEl.height };
133
+ };
134
+ /** Viewport (client) pixels → document pixels. */
135
+ const clientToDoc = (p: PlaybackPoint): PlaybackPoint => {
136
+ if (!canvasEl || !canvasEl.width) return p;
137
+ const r = canvasEl.getBoundingClientRect();
138
+ const sx = r.width > 0 ? canvasEl.width / r.width : 1;
139
+ const sy = r.height > 0 ? canvasEl.height / r.height : 1;
140
+ return canvasToImagePoint({ x: (p.x - r.left) * sx, y: (p.y - r.top) * sy }, editor.controller.getState(), canvasEl.width, canvasEl.height, activeTab === 'crop');
141
+ };
142
+ /** An element of the editor (a tab, slider or layer row) as a rectangle in document pixels. */
143
+ const elementRect = (selector: string): PlaybackRect | null => {
144
+ const r = rootEl?.querySelector(selector)?.getBoundingClientRect();
145
+ if (!r || r.width <= 0) return null;
146
+ const a = clientToDoc({ x: r.left, y: r.top });
147
+ const b = clientToDoc({ x: r.right, y: r.bottom });
148
+ return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
149
+ };
150
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
151
+ defaultKind: 'layer',
152
+ locate: (ref: PlaybackRef) => {
153
+ if (ref.kind === 'panel') return elementRect(`[data-image-tab="${CSS.escape(ref.id)}"]`);
154
+ if (ref.kind === 'control') return elementRect(`[data-control-id="${CSS.escape(ref.id)}"]`);
155
+ return createImagePlaybackLocator(editor.controller)(ref) ?? (ref.kind === 'layer' || ref.kind === 'item' ? elementRect(`[data-layer-id="${CSS.escape(ref.id)}"]`) : null);
156
+ },
157
+ open: async (ref: PlaybackRef) => {
158
+ if (ref.kind !== 'panel' || activeTab === ref.id) return;
159
+ rootEl?.querySelector<HTMLElement>(`[data-image-tab="${CSS.escape(ref.id)}"]`)?.click();
160
+ // Let the panel render before the cursor looks for its controls.
161
+ await new Promise((resolve) => setTimeout(resolve, 60));
162
+ },
163
+ bounds: () => {
164
+ const r = canvasEl?.getBoundingClientRect();
165
+ if (!r || r.width <= 0) return null;
166
+ const a = clientToDoc({ x: r.left, y: r.top });
167
+ const b = clientToDoc({ x: r.right, y: r.bottom });
168
+ return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
169
+ },
170
+ visible: () => {
171
+ const r = rootEl?.getBoundingClientRect();
172
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
173
+ },
174
+ toViewport: (p: PlaybackPoint) => {
175
+ const box = rootEl?.getBoundingClientRect();
176
+ const q = docToRoot(p);
177
+ return { x: (box?.left ?? 0) + q.x, y: (box?.top ?? 0) + q.y };
178
+ },
179
+ fromViewport: clientToDoc,
180
+ reveal: () => onplaybackreveal?.()
181
+ }));
104
182
 
105
183
  // Selection tools (magic wand, marquees, lasso): options bar state and the pointer gesture.
106
184
  let selectionOptions = $state<SelectionToolOptions>({ ...DEFAULT_SELECTION_TOOL_OPTIONS });
@@ -883,15 +961,20 @@
883
961
  </script>
884
962
 
885
963
  <div
886
- class="flex flex-col bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl text-slate-100 {className}"
964
+ bind:this={rootEl}
965
+ class="relative flex flex-col bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl text-slate-100 {className}"
887
966
  {style}
888
967
  >
968
+ {#if playing}
969
+ <PresenceLayer presence={agentPresence} viewport={docToRoot} />
970
+ {/if}
889
971
  <!-- Top Action Header -->
890
972
  <div class="flex flex-wrap items-center justify-between px-4 py-3 bg-slate-950/80 border-b border-slate-800 gap-3">
891
973
  <!-- Navigation Tabs -->
892
974
  <div class="flex items-center gap-1 bg-slate-900 p-1 rounded-lg border border-slate-800 text-xs">
893
975
  <button
894
976
  type="button"
977
+ data-image-tab="crop"
895
978
  onclick={() => {
896
979
  activeTab = 'crop';
897
980
  editor.setTool('crop');
@@ -904,6 +987,7 @@
904
987
  </button>
905
988
  <button
906
989
  type="button"
990
+ data-image-tab="adjust"
907
991
  onclick={() => {
908
992
  activeTab = 'adjust';
909
993
  editor.setTool('select');
@@ -916,6 +1000,7 @@
916
1000
  </button>
917
1001
  <button
918
1002
  type="button"
1003
+ data-image-tab="layers"
919
1004
  onclick={() => {
920
1005
  activeTab = 'layers';
921
1006
  editor.setTool('select');
@@ -928,6 +1013,7 @@
928
1013
  </button>
929
1014
  <button
930
1015
  type="button"
1016
+ data-image-tab="paint"
931
1017
  onclick={() => {
932
1018
  activeTab = 'paint';
933
1019
  editor.setTool('brush');
@@ -940,6 +1026,7 @@
940
1026
  </button>
941
1027
  <button
942
1028
  type="button"
1029
+ data-image-tab="select"
943
1030
  onclick={() => {
944
1031
  activeTab = 'select';
945
1032
  editor.setTool('select');
@@ -952,6 +1039,7 @@
952
1039
  </button>
953
1040
  <button
954
1041
  type="button"
1042
+ data-image-tab="annotate"
955
1043
  onclick={() => {
956
1044
  activeTab = 'annotate';
957
1045
  editor.setTool('pen');
@@ -964,6 +1052,7 @@
964
1052
  </button>
965
1053
  <button
966
1054
  type="button"
1055
+ data-image-tab="depth"
967
1056
  onclick={() => {
968
1057
  activeTab = 'depth';
969
1058
  editor.setTool('select');
@@ -976,6 +1065,7 @@
976
1065
  </button>
977
1066
  <button
978
1067
  type="button"
1068
+ data-image-tab="ai"
979
1069
  onclick={() => {
980
1070
  activeTab = 'ai';
981
1071
  editor.setTool('select');
@@ -1,4 +1,5 @@
1
1
  import type { ExportOptions, InpaintHook, RemoveBackgroundHook, ImageEditorAIHooks, ImageEditorFit } from '@pixerate/editor/image-editor';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  import { type ReactiveImageEditor } from './imageEditorState.svelte.js';
3
4
  import type { EffectProvider } from '@pixerate/editor/effects';
4
5
  interface Props {
@@ -18,6 +19,15 @@ interface Props {
18
19
  * with an Effects section in the Layers tab. Optional: without one the editor is as before.
19
20
  */
20
21
  effects?: EffectProvider | null;
22
+ /**
23
+ * Agent playback: act out the image tools' calls here with the agent's cursor, opening the
24
+ * panel each tool belongs to (default: `setAgentPlayback` above; `null` turns it off).
25
+ */
26
+ playback?: AgentPlayback | null;
27
+ /** The playback surface id (default `image`, where the image tool providers play). */
28
+ playbackId?: string;
29
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
30
+ onplaybackreveal?: () => void | Promise<void>;
21
31
  }
22
32
  declare const ImageEditor: import("svelte").Component<Props, {}, "">;
23
33
  type ImageEditor = ReturnType<typeof ImageEditor>;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./history/index.js";
14
14
  export * from "./fileImport.svelte.js";
15
15
  export * from "./docxExport.svelte.js";
16
16
  export * from "./agentTools.svelte.js";
17
+ export * from "./playback/index.js";
17
18
  export * from "./toolBridge.svelte.js";
18
19
  export * from "./toolRegistryView.svelte.js";
19
20
  export * from "./outline.svelte.js";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./history/index.js";
5
5
  export * from "./fileImport.svelte.js";
6
6
  export * from "./docxExport.svelte.js";
7
7
  export * from "./agentTools.svelte.js";
8
+ export * from "./playback/index.js";
8
9
  export * from "./toolBridge.svelte.js";
9
10
  export * from "./toolRegistryView.svelte.js";
10
11
  export * from "./outline.svelte.js";