@pixerate/editor-svelte 0.102.0 → 0.103.2

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.
@@ -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>;
@@ -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>;
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
- import type { PaintEditorController, PaintPresence, PaintTool } from '@pixerate/editor/paint-editor';
2
+ import { PaintPresence, documentToScreen, screenToDocument, type PaintEditorController, type PaintTool } from '@pixerate/editor/paint-editor';
3
+ import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
4
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
5
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
3
6
  import BrushPanel from './BrushPanel.svelte';
4
7
  import ColorPanel from './ColorPanel.svelte';
5
8
  import PaintCanvas from './PaintCanvas.svelte';
@@ -20,8 +23,81 @@
20
23
  /** Agent cursors to draw over the canvas (see `createPaintDirector`). */
21
24
  presence?: PaintPresence;
22
25
  class?: string;
26
+ /**
27
+ * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
28
+ * panel each write belongs to (default: `setAgentPlayback` above; `null` turns it off). With
29
+ * `presence` (a `PaintDirector`'s), the director's strokes share the cursor.
30
+ */
31
+ playback?: AgentPlayback | null;
32
+ /** The playback surface id (default `paint`, where the paint tool providers play). */
33
+ playbackId?: string;
34
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
35
+ onplaybackreveal?: () => void | Promise<void>;
23
36
  }
24
- let { controller, fileName, height = 720, presence, class: className = '' }: Props = $props();
37
+ let { controller, fileName, height = 720, presence, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
38
+
39
+ /** Controls a paint plan can point at, by `{ kind: 'control', id }`. */
40
+ const PAINT_CONTROLS: Record<string, string> = {
41
+ 'mixing-pad': '[data-testid="mixing-pad"]',
42
+ lighting: '[data-testid="light-panel"]',
43
+ symmetry: '[aria-label="Symmetry"]'
44
+ };
45
+ // Agent playback in document pixels, sharing a director's presence store (one cursor). Panels
46
+ // outside the canvas are mapped into document pixels through the canvas view.
47
+ let root: HTMLDivElement | undefined = $state();
48
+ const ambientPlayback = getAgentPlayback();
49
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
50
+ const viewBox = () => {
51
+ const r = root?.querySelector<HTMLElement>('[data-testid="paint-canvas"]')?.getBoundingClientRect();
52
+ return r && r.width > 0 ? r : null;
53
+ };
54
+ const docToClient = (p: PlaybackPoint): PlaybackPoint | null => {
55
+ const r = viewBox();
56
+ if (!r) return null;
57
+ const q = documentToScreen(controller.getState().view, controller.document.canvas, { width: r.width, height: r.height }, p.x, p.y);
58
+ return { x: r.left + q.x, y: r.top + q.y };
59
+ };
60
+ const clientToDoc = (p: PlaybackPoint): PlaybackPoint | null => {
61
+ const r = viewBox();
62
+ return r ? screenToDocument(controller.getState().view, controller.document.canvas, { width: r.width, height: r.height }, p.x - r.left, p.y - r.top) : null;
63
+ };
64
+ const elementRect = (selector: string): PlaybackRect | null => {
65
+ const r = root?.querySelector(selector)?.getBoundingClientRect();
66
+ if (!r || r.width <= 0) return null;
67
+ // The element's centre and size (the view may rotate; the cursor only needs the centre).
68
+ const c = clientToDoc({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
69
+ const zoom = controller.getState().view.zoom || 1;
70
+ return c ? { x: c.x - r.width / zoom / 2, y: c.y - r.height / zoom / 2, width: r.width / zoom, height: r.height / zoom } : null;
71
+ };
72
+ // svelte-ignore state_referenced_locally
73
+ const sharedPresence = presence ?? new PaintPresence();
74
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
75
+ locate: (ref: PlaybackRef) => {
76
+ if (ref.kind === 'panel') return elementRect(`[data-paint-tab="${CSS.escape(ref.id)}"]`);
77
+ if (ref.kind === 'panel-body') return elementRect(`[data-paint-panel="${CSS.escape(ref.id)}"]`);
78
+ if (ref.kind === 'control' && PAINT_CONTROLS[ref.id]) return elementRect(PAINT_CONTROLS[ref.id]!);
79
+ return null;
80
+ },
81
+ open: async (ref: PlaybackRef) => {
82
+ if (ref.kind !== 'panel' || tab === ref.id) return;
83
+ root?.querySelector<HTMLElement>(`[data-paint-tab="${CSS.escape(ref.id)}"]`)?.click();
84
+ await new Promise((resolve) => setTimeout(resolve, 60));
85
+ },
86
+ bounds: () => ({ x: 0, y: 0, width: controller.document.canvas.width, height: controller.document.canvas.height }),
87
+ visible: () => {
88
+ const r = root?.getBoundingClientRect();
89
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
90
+ },
91
+ toViewport: (p: PlaybackPoint) => docToClient(p) ?? p,
92
+ fromViewport: (p: PlaybackPoint) => clientToDoc(p) ?? p,
93
+ reveal: () => onplaybackreveal?.()
94
+ }), sharedPresence);
95
+ /** Document pixels → pixels in the editor's root, for the cursor layer. */
96
+ const docToRoot = (p: PlaybackPoint): PlaybackPoint => {
97
+ const c = docToClient(p);
98
+ const box = root?.getBoundingClientRect();
99
+ return c && box ? { x: c.x - box.left, y: c.y - box.top } : p;
100
+ };
25
101
  let tool = $state<PaintTool>('brush');
26
102
  let tab = $state<'color' | 'wet' | 'paper' | 'layers' | 'effects'>('color');
27
103
  const effects = paintEffectsProvider(() => controller);
@@ -30,25 +106,31 @@
30
106
  </script>
31
107
 
32
108
  <!-- Toolbar, brush library, the canvas, and colour / wet media / paper / layers. -->
33
- <div class={`flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className}`} style={`height: ${typeof height === 'number' ? `${height}px` : height}`} data-testid="paint-editor">
109
+ <div bind:this={root} class={`relative flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className}`} style={`height: ${typeof height === 'number' ? `${height}px` : height}`} data-testid="paint-editor">
110
+ {#if playing}
111
+ <PresenceLayer presence={agentPresence} viewport={docToRoot} />
112
+ {/if}
34
113
  <PaintToolbar {controller} {tool} ontoolchange={(t) => (tool = t)} {fileName} />
35
114
  <div class="flex min-h-0 flex-1 gap-2">
36
115
  <BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
37
116
  <div class="flex min-w-0 flex-1 flex-col">
38
- <PaintCanvas {controller} {tool} {presence} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
117
+ <!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
118
+ <PaintCanvas {controller} {tool} presence={playing ? undefined : presence} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
39
119
  {#if error}<p role="alert" class="mt-1 text-[11px] text-rose-400">{error}</p>{/if}
40
120
  </div>
41
121
  <div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
42
122
  <div class="flex gap-1" role="tablist" aria-label="Paint panels">
43
123
  {#each tabs as t (t)}
44
- <button type="button" role="tab" aria-selected={tab === t} class={chip(tab === t)} onclick={() => (tab = t)}>{t === 'color' ? 'Colour' : t === 'wet' ? 'Wet' : t === 'paper' ? 'Paper' : t === 'layers' ? 'Layers' : 'Effects'}</button>
124
+ <button type="button" role="tab" aria-selected={tab === t} data-paint-tab={t} class={chip(tab === t)} onclick={() => (tab = t)}>{t === 'color' ? 'Colour' : t === 'wet' ? 'Wet' : t === 'paper' ? 'Paper' : t === 'layers' ? 'Layers' : 'Effects'}</button>
45
125
  {/each}
46
126
  </div>
47
- {#if tab === 'color'}<ColorPanel {controller} />{/if}
48
- {#if tab === 'wet'}<WetPanel {controller} />{/if}
49
- {#if tab === 'paper'}<PaperPanel {controller} />{/if}
50
- {#if tab === 'layers'}<PaintLayersPanel {controller} /><StickerPanel {controller} />{/if}
51
- {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
127
+ <div class="flex flex-col gap-2" data-paint-panel={tab}>
128
+ {#if tab === 'color'}<ColorPanel {controller} />{/if}
129
+ {#if tab === 'wet'}<WetPanel {controller} />{/if}
130
+ {#if tab === 'paper'}<PaperPanel {controller} />{/if}
131
+ {#if tab === 'layers'}<PaintLayersPanel {controller} /><StickerPanel {controller} />{/if}
132
+ {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
133
+ </div>
52
134
  </div>
53
135
  </div>
54
136
  </div>
@@ -1,4 +1,5 @@
1
- import type { PaintEditorController, PaintPresence } from '@pixerate/editor/paint-editor';
1
+ import { PaintPresence, type PaintEditorController } from '@pixerate/editor/paint-editor';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  interface Props {
3
4
  controller: PaintEditorController;
4
5
  /** Base name of saved and exported files (default `painting`). */
@@ -8,6 +9,16 @@ interface Props {
8
9
  /** Agent cursors to draw over the canvas (see `createPaintDirector`). */
9
10
  presence?: PaintPresence;
10
11
  class?: string;
12
+ /**
13
+ * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
14
+ * panel each write belongs to (default: `setAgentPlayback` above; `null` turns it off). With
15
+ * `presence` (a `PaintDirector`'s), the director's strokes share the cursor.
16
+ */
17
+ playback?: AgentPlayback | null;
18
+ /** The playback surface id (default `paint`, where the paint tool providers play). */
19
+ playbackId?: string;
20
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
21
+ onplaybackreveal?: () => void | Promise<void>;
11
22
  }
12
23
  declare const PaintEditor: import("svelte").Component<Props, {}, "">;
13
24
  type PaintEditor = ReturnType<typeof PaintEditor>;
@@ -6,6 +6,9 @@
6
6
 
7
7
  <script lang="ts">
8
8
  import { untrack, type Snippet } from 'svelte';
9
+ import type { AgentPlayback } from '@pixerate/editor/tools';
10
+ import { createElementPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
11
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
9
12
  import {
10
13
  isParameterGroup,
11
14
  parameterLabel,
@@ -40,6 +43,15 @@
40
43
  class?: string;
41
44
  /** Extra content after the fields. */
42
45
  children?: Snippet;
46
+ /**
47
+ * Agent playback: act out `parameters.*` commands here with the agent's cursor (default:
48
+ * `setAgentPlayback` above; `null` turns it off).
49
+ */
50
+ playback?: AgentPlayback | null;
51
+ /** The playback surface id (default `parameters`, where the parameter commands play). */
52
+ playbackId?: string;
53
+ /** Bring the panel on screen (switch to its tab) before the agent acts. */
54
+ onplaybackreveal?: () => void | Promise<void>;
43
55
  }
44
56
 
45
57
  let {
@@ -53,9 +65,22 @@
53
65
  emptyMessage = 'No parameters.',
54
66
  idPrefix = `pixerate-parameters-${++nextId}`,
55
67
  class: className = '',
56
- children
68
+ children,
69
+ playback,
70
+ playbackId = 'parameters',
71
+ onplaybackreveal
57
72
  }: Props = $props();
58
73
 
74
+ // Agent playback: the cursor visits the controls a call changes.
75
+ let root: HTMLDivElement | undefined = $state();
76
+ const ambientPlayback = getAgentPlayback();
77
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
78
+ const agentPresence = createElementPlaybackSurface(() => playing, () => playbackId, () => root, () => ({
79
+ defaultKind: 'parameter',
80
+ selectors: { parameter: (id: string) => `[data-parameter-id="${CSS.escape(id)}"]` },
81
+ reveal: onplaybackreveal
82
+ }));
83
+
59
84
  // Follows a replaced `controller` prop too.
60
85
  let view: ParametersState = $state.raw(untrack(() => controller.getState()));
61
86
  $effect(() => {
@@ -164,7 +189,7 @@
164
189
  keyframe diamonds, per-field reset, presets, and undo / redo / reset / randomize. Slider
165
190
  drags and colour picks are one undo step each.
166
191
  -->
167
- <div class="space-y-3 text-foreground {className}" data-parameters-panel="">
192
+ <div bind:this={root} class="relative space-y-3 text-foreground {className}" data-parameters-panel="">
168
193
  {#if heading || showToolbar}
169
194
  <div class="flex flex-wrap items-center gap-1.5">
170
195
  {#if heading}
@@ -236,4 +261,8 @@
236
261
  {@render items(view.schema.fields)}
237
262
  {/if}
238
263
  {@render children?.()}
264
+ <!-- Last, so it doesn't shift the stacked spacing. -->
265
+ {#if playing}
266
+ <PresenceLayer presence={agentPresence} />
267
+ {/if}
239
268
  </div>
@@ -1,4 +1,5 @@
1
1
  import { type Snippet } from 'svelte';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  import { type ParametersController } from '@pixerate/editor/parameters';
3
4
  interface Props {
4
5
  controller: ParametersController;
@@ -21,6 +22,15 @@ interface Props {
21
22
  class?: string;
22
23
  /** Extra content after the fields. */
23
24
  children?: Snippet;
25
+ /**
26
+ * Agent playback: act out `parameters.*` commands here with the agent's cursor (default:
27
+ * `setAgentPlayback` above; `null` turns it off).
28
+ */
29
+ playback?: AgentPlayback | null;
30
+ /** The playback surface id (default `parameters`, where the parameter commands play). */
31
+ playbackId?: string;
32
+ /** Bring the panel on screen (switch to its tab) before the agent acts. */
33
+ onplaybackreveal?: () => void | Promise<void>;
24
34
  }
25
35
  declare const ParametersPanel: import("svelte").Component<Props, {}, "">;
26
36
  type ParametersPanel = ReturnType<typeof ParametersPanel>;
@@ -1,2 +1,2 @@
1
1
  export { default as PresenceLayer } from './PresenceLayer.svelte';
2
- export { setAgentPlayback, getAgentPlayback, createPlaybackSurface, type PlaybackSurfaceOptions } from './playback.svelte.js';
2
+ export { setAgentPlayback, getAgentPlayback, createPlaybackSurface, createElementPlaybackSurface, type PlaybackSurfaceOptions, type ElementPlaybackSurfaceOptions } from './playback.svelte.js';
@@ -1,2 +1,2 @@
1
1
  export { default as PresenceLayer } from './PresenceLayer.svelte';
2
- export { setAgentPlayback, getAgentPlayback, createPlaybackSurface } from './playback.svelte.js';
2
+ export { setAgentPlayback, getAgentPlayback, createPlaybackSurface, createElementPlaybackSurface } from './playback.svelte.js';
@@ -12,5 +12,21 @@ export type PlaybackSurfaceOptions = Omit<PlaybackSurface, 'presence'>;
12
12
  * `<PresenceLayer>`. `playback` defaults to `getAgentPlayback()`; with none, nothing is attached and
13
13
  * calls run instantly. Parity with React's `usePlaybackSurface`.
14
14
  */
15
- export declare function createPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options?: MaybeGetter<PlaybackSurfaceOptions>): PresenceStore;
15
+ export declare function createPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options?: MaybeGetter<PlaybackSurfaceOptions>,
16
+ /** Use this presence store (e.g. a director's, so its cursor and playback's are one) instead of a new one. */
17
+ sharedPresence?: PresenceStore | null): PresenceStore;
18
+ export interface ElementPlaybackSurfaceOptions {
19
+ /** The kind of a bare `id` / `ids` argument (e.g. `parameter`). */
20
+ defaultKind?: string;
21
+ /** A CSS selector for each ref kind the surface can find inside the element. */
22
+ selectors: Record<string, (id: string) => string>;
23
+ /** Bring the element on screen (switch to its tab) before the agent acts. */
24
+ reveal?: () => void | Promise<void>;
25
+ }
26
+ /**
27
+ * A playback surface for a plain DOM component (call during init): coordinates are pixels of the
28
+ * element's box, and refs are found by the CSS selectors given per kind. Draw the cursors with
29
+ * `<PresenceLayer>` inside the element. Parity with React's `useElementPlaybackSurface`.
30
+ */
31
+ export declare function createElementPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, element: () => HTMLElement | null | undefined, options: MaybeGetter<ElementPlaybackSurfaceOptions>): PresenceStore;
16
32
  export {};
@@ -16,9 +16,11 @@ export function getAgentPlayback() {
16
16
  * `<PresenceLayer>`. `playback` defaults to `getAgentPlayback()`; with none, nothing is attached and
17
17
  * calls run instantly. Parity with React's `usePlaybackSurface`.
18
18
  */
19
- export function createPlaybackSurface(playback, id, options = {}) {
19
+ export function createPlaybackSurface(playback, id, options = {},
20
+ /** Use this presence store (e.g. a director's, so its cursor and playback's are one) instead of a new one. */
21
+ sharedPresence) {
20
22
  const ambient = getAgentPlayback();
21
- const presence = new PresenceStore();
23
+ const presence = sharedPresence ?? new PresenceStore();
22
24
  const o = () => read(options);
23
25
  $effect(() => {
24
26
  const explicit = read(playback);
@@ -50,3 +52,40 @@ export function createPlaybackSurface(playback, id, options = {}) {
50
52
  });
51
53
  return presence;
52
54
  }
55
+ /**
56
+ * A playback surface for a plain DOM component (call during init): coordinates are pixels of the
57
+ * element's box, and refs are found by the CSS selectors given per kind. Draw the cursors with
58
+ * `<PresenceLayer>` inside the element. Parity with React's `useElementPlaybackSurface`.
59
+ */
60
+ export function createElementPlaybackSurface(playback, id, element, options) {
61
+ const box = () => element()?.getBoundingClientRect() ?? null;
62
+ return createPlaybackSurface(playback, id, () => {
63
+ const o = read(options);
64
+ return {
65
+ defaultKind: o.defaultKind,
66
+ locate: (ref) => {
67
+ const selector = o.selectors[ref.kind]?.(ref.id);
68
+ const b = box();
69
+ const r = selector ? element()?.querySelector(selector)?.getBoundingClientRect() : null;
70
+ return b && r && r.width > 0 ? { x: r.left - b.left, y: r.top - b.top, width: r.width, height: r.height } : null;
71
+ },
72
+ bounds: () => {
73
+ const r = box();
74
+ return r && r.width > 0 ? { x: 0, y: 0, width: r.width, height: r.height } : null;
75
+ },
76
+ visible: () => {
77
+ const r = box();
78
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
79
+ },
80
+ toViewport: (p) => {
81
+ const r = box();
82
+ return { x: (r?.left ?? 0) + p.x, y: (r?.top ?? 0) + p.y };
83
+ },
84
+ fromViewport: (p) => {
85
+ const r = box();
86
+ return { x: p.x - (r?.left ?? 0), y: p.y - (r?.top ?? 0) };
87
+ },
88
+ reveal: () => o.reveal?.()
89
+ };
90
+ });
91
+ }
@@ -20,8 +20,12 @@
20
20
  getDataBarCss,
21
21
  conditionalFormatStyleString,
22
22
  type SpreadsheetPanelId,
23
- type SpreadsheetValidationAction
23
+ type SpreadsheetValidationAction,
24
+ parseSpreadsheetPlaybackRef
24
25
  } from '@pixerate/editor/spreadsheet';
26
+ import type { AgentPlayback, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
27
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
28
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
25
29
  import FormulaBar from './FormulaBar.svelte';
26
30
  import SpreadsheetValidationDialog from './SpreadsheetValidationDialog.svelte';
27
31
  import SpreadsheetConditionalFormatPanel from './SpreadsheetConditionalFormatPanel.svelte';
@@ -54,6 +58,15 @@
54
58
  * `true` / `false` force it.
55
59
  */
56
60
  virtualize?: boolean | 'auto';
61
+ /**
62
+ * Agent playback: act out the spreadsheet tools' calls here with the agent's cursor (default:
63
+ * `setAgentPlayback` above; `null` turns it off).
64
+ */
65
+ playback?: AgentPlayback | null;
66
+ /** The playback surface id (default `spreadsheet`, where `createSpreadsheetToolProvider` plays). */
67
+ playbackId?: string;
68
+ /** Bring the grid on screen (switch to its tab) before the agent acts. */
69
+ onplaybackreveal?: () => void | Promise<void>;
57
70
  }
58
71
 
59
72
  let {
@@ -67,7 +80,10 @@
67
80
  onColumnHeaderClick,
68
81
  onColumnResize,
69
82
  showSheetTabs = false,
70
- virtualize = 'auto'
83
+ virtualize = 'auto',
84
+ playback,
85
+ playbackId = 'spreadsheet',
86
+ onplaybackreveal
71
87
  }: Props = $props();
72
88
 
73
89
  // Virtualised rendering: frozen rows/columns, a spacer, the rows and
@@ -120,6 +136,47 @@
120
136
  let isReadOnly = $derived(readOnly || readonly);
121
137
 
122
138
  let gridContainer = $state<HTMLDivElement | null>(null);
139
+
140
+ // Agent playback: the agent's cursor in pixels of the grid's box, finding cells by data-row / data-col.
141
+ const ambientPlayback = getAgentPlayback();
142
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
143
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
144
+ defaultKind: 'cell',
145
+ locate: (ref: PlaybackRef): PlaybackRect | null => {
146
+ const grid = gridContainer;
147
+ const range = parseSpreadsheetPlaybackRef(ref);
148
+ if (!grid || !range) return null;
149
+ const box = grid.getBoundingClientRect();
150
+ const inside = (v: number, a: number | null, b: number | null) => (a === null || v >= a) && (b === null || v <= b);
151
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
152
+ for (const td of grid.querySelectorAll<HTMLElement>('td[data-row][data-col]')) {
153
+ if (!inside(Number(td.dataset.row), range.startRow, range.endRow) || !inside(Number(td.dataset.col), range.startCol, range.endCol)) continue;
154
+ const r = td.getBoundingClientRect();
155
+ minX = Math.min(minX, r.left - box.left);
156
+ minY = Math.min(minY, r.top - box.top);
157
+ maxX = Math.max(maxX, r.right - box.left);
158
+ maxY = Math.max(maxY, r.bottom - box.top);
159
+ }
160
+ return minX < maxX ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY } : null;
161
+ },
162
+ bounds: () => {
163
+ const r = gridContainer?.getBoundingClientRect();
164
+ return r && r.width > 0 ? { x: 0, y: 0, width: r.width, height: r.height } : null;
165
+ },
166
+ visible: () => {
167
+ const r = gridContainer?.getBoundingClientRect();
168
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
169
+ },
170
+ toViewport: (p: { x: number; y: number }) => {
171
+ const r = gridContainer?.getBoundingClientRect();
172
+ return { x: (r?.left ?? 0) + p.x, y: (r?.top ?? 0) + p.y };
173
+ },
174
+ fromViewport: (p: { x: number; y: number }) => {
175
+ const r = gridContainer?.getBoundingClientRect();
176
+ return { x: p.x - (r?.left ?? 0), y: p.y - (r?.top ?? 0) };
177
+ },
178
+ reveal: () => onplaybackreveal?.()
179
+ }));
123
180
  let cellInput = $state<HTMLInputElement | null>(null);
124
181
 
125
182
  // Focus the cell editor when it opens, with the caret after the draft
@@ -586,7 +643,7 @@
586
643
  {#snippet grid()}
587
644
  <div
588
645
  bind:this={gridContainer}
589
- class="flex flex-col h-full w-full overflow-hidden border border-border bg-background select-none outline-none text-foreground font-sans text-xs {className}"
646
+ class="relative flex flex-col h-full w-full overflow-hidden border border-border bg-background select-none outline-none text-foreground font-sans text-xs {className}"
590
647
  tabindex="0"
591
648
  onkeydown={handleKeyDown}
592
649
  role="grid"
@@ -879,6 +936,9 @@
879
936
  {#if sheetState.validationPrompt}
880
937
  <SpreadsheetValidationDialog prompt={sheetState.validationPrompt} onResolve={resolveValidation} />
881
938
  {/if}
939
+ {#if playing}
940
+ <PresenceLayer presence={agentPresence} />
941
+ {/if}
882
942
  </div>
883
943
  {/snippet}
884
944
 
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import { type SpreadsheetColumn, type SpreadsheetRow, type CellData } from '@pixerate/editor/spreadsheet';
3
+ import type { AgentPlayback } from '@pixerate/editor/tools';
3
4
  import { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
4
5
  interface Props {
5
6
  sheetState?: ReturnType<typeof createReactiveSpreadsheet>;
@@ -23,6 +24,15 @@ interface Props {
23
24
  * `true` / `false` force it.
24
25
  */
25
26
  virtualize?: boolean | 'auto';
27
+ /**
28
+ * Agent playback: act out the spreadsheet tools' calls here with the agent's cursor (default:
29
+ * `setAgentPlayback` above; `null` turns it off).
30
+ */
31
+ playback?: AgentPlayback | null;
32
+ /** The playback surface id (default `spreadsheet`, where `createSpreadsheetToolProvider` plays). */
33
+ playbackId?: string;
34
+ /** Bring the grid on screen (switch to its tab) before the agent acts. */
35
+ onplaybackreveal?: () => void | Promise<void>;
26
36
  }
27
37
  declare const SpreadsheetEditor: import("svelte").Component<Props, {}, "">;
28
38
  type SpreadsheetEditor = ReturnType<typeof SpreadsheetEditor>;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  import { onDestroy } from 'svelte';
3
4
  import { VideoEditorController, type VideoEditorOptions } from '@pixerate/editor/video-editor';
4
5
  import ClipInspector from './ClipInspector.svelte';
@@ -40,9 +41,13 @@
40
41
  /** Show the effects panel (default: when a provider is given or the project has effects). */
41
42
  showEffects?: boolean;
42
43
  class?: string;
44
+ /** Agent playback on the timeline (see `<VideoTimeline playback>`; default: `setAgentPlayback` above). */
45
+ playback?: AgentPlayback | null;
46
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
47
+ onplaybackreveal?: () => void | Promise<void>;
43
48
  }
44
49
 
45
- let { controller, options, editable = true, pxPerSecond = $bindable(100), showIssues = true, showTranscript = true, showScopes = false, effects, showEffects, class: className = '' }: Props = $props();
50
+ let { controller, options, editable = true, pxPerSecond = $bindable(100), showIssues = true, showTranscript = true, showScopes = false, effects, showEffects, class: className = '', playback, onplaybackreveal }: Props = $props();
46
51
  let owned: VideoEditorController | null = null;
47
52
  // A controller is made only when none is passed, and kept for the component's lifetime.
48
53
  const ctl = $derived(controller ?? (owned ??= new VideoEditorController(options)));
@@ -73,7 +78,7 @@
73
78
  <VideoPreview controller={ctl} class="rounded-md" />
74
79
  <VideoTransport controller={ctl} editable={false} />
75
80
  {#if showScopes}<VideoScopes controller={ctl} />{/if}
76
- <VideoTimeline controller={ctl} bind:pxPerSecond editable={false} />
81
+ <VideoTimeline controller={ctl} bind:pxPerSecond editable={false} {playback} {onplaybackreveal} />
77
82
  {#if showTranscript}<TranscriptPanel controller={ctl} editable={false} />{/if}
78
83
  {@render issueList()}
79
84
  </div>
@@ -90,7 +95,7 @@
90
95
  {#if effectsShown}<VideoEffectsPanel controller={ctl} class="min-w-0" />{/if}
91
96
  </div>
92
97
  <div class="flex min-w-0 flex-col gap-2 lg:col-span-3">
93
- <VideoTimeline controller={ctl} bind:pxPerSecond />
98
+ <VideoTimeline controller={ctl} bind:pxPerSecond {playback} {onplaybackreveal} />
94
99
  {#if showTranscript}<TranscriptPanel controller={ctl} />{/if}
95
100
  {@render issueList()}
96
101
  </div>
@@ -1,3 +1,4 @@
1
+ import type { AgentPlayback } from '@pixerate/editor/tools';
1
2
  import { VideoEditorController, type VideoEditorOptions } from '@pixerate/editor/video-editor';
2
3
  import type { EffectProvider } from '@pixerate/editor/effects';
3
4
  interface Props {
@@ -27,6 +28,10 @@ interface Props {
27
28
  /** Show the effects panel (default: when a provider is given or the project has effects). */
28
29
  showEffects?: boolean;
29
30
  class?: string;
31
+ /** Agent playback on the timeline (see `<VideoTimeline playback>`; default: `setAgentPlayback` above). */
32
+ playback?: AgentPlayback | null;
33
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
34
+ onplaybackreveal?: () => void | Promise<void>;
30
35
  }
31
36
  declare const VideoEditor: import("svelte").Component<Props, {}, "pxPerSecond">;
32
37
  type VideoEditor = ReturnType<typeof VideoEditor>;
@@ -20,7 +20,11 @@
20
20
  type VideoMediaClip,
21
21
  type VideoTrack,
22
22
  type VideoWaveformMode,
23
+ parseVideoTimeRef,
23
24
  } from '@pixerate/editor/video-editor';
25
+ import type { AgentPlayback, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
26
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
27
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
24
28
  import { onDestroy, untrack } from 'svelte';
25
29
  import { videoEditorStateOf } from './createVideoEditor.svelte.js';
26
30
 
@@ -35,9 +39,18 @@
35
39
  /** Draw waveforms in audio clips (default), in every clip with sound (`all`), or not at all. Sound is decoded once per asset through `controller.audioPeaks`. */
36
40
  waveforms?: VideoWaveformMode;
37
41
  class?: string;
42
+ /**
43
+ * Agent playback: act out the video tools' calls here with the agent's cursor (default:
44
+ * `setAgentPlayback` above; `null` turns it off).
45
+ */
46
+ playback?: AgentPlayback | null;
47
+ /** The playback surface id (default `video`, where the video tool providers play). */
48
+ playbackId?: string;
49
+ /** Bring the timeline on screen (switch to its tab) before the agent acts. */
50
+ onplaybackreveal?: () => void | Promise<void>;
38
51
  }
39
52
 
40
- let { controller, pxPerSecond = $bindable(100), onpxpersecondchange, editable = true, waveforms = 'audio', class: className = '' }: Props = $props();
53
+ let { controller, pxPerSecond = $bindable(100), onpxpersecondchange, editable = true, waveforms = 'audio', class: className = '', playback, playbackId = 'video', onplaybackreveal }: Props = $props();
41
54
  /** Bumped when more sound peaks have loaded, so waveforms redraw. */
42
55
  let peaksVersion = $state(0);
43
56
  $effect(() => controller.audioPeaks.subscribe(() => peaksVersion++));
@@ -90,6 +103,51 @@
90
103
  const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
91
104
 
92
105
  let container: HTMLDivElement | undefined = $state();
106
+
107
+ // Agent playback: the agent's cursor in pixels of the timeline's box, finding clips, tracks and times.
108
+ const ambientPlayback = getAgentPlayback();
109
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
110
+ const rectIn = (el: Element | null | undefined): PlaybackRect | null => {
111
+ const box = container?.getBoundingClientRect();
112
+ const r = el?.getBoundingClientRect();
113
+ return box && r && r.width > 0 ? { x: r.left - box.left, y: r.top - box.top, width: r.width, height: r.height } : null;
114
+ };
115
+ const q = (selector: string) => container?.querySelector(selector);
116
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
117
+ defaultKind: 'clip',
118
+ locate: (ref: PlaybackRef) => {
119
+ if (ref.kind === 'clip') return rectIn(q(`[data-clip="${CSS.escape(ref.id)}"]`));
120
+ if (ref.kind === 'track') return rectIn(q(`[data-track-lane="${CSS.escape(ref.id)}"]`));
121
+ if (ref.kind === 'marker') return rectIn(q(`[data-marker="${CSS.escape(ref.id)}"]`));
122
+ const at = parseVideoTimeRef(ref);
123
+ if (!at) return null;
124
+ // The row: the named track, the row of the named clip, else the ruler.
125
+ const lane = at.track
126
+ ? q(`[data-track-lane="${CSS.escape(at.track)}"]`)
127
+ : at.clip
128
+ ? q(`[data-clip="${CSS.escape(at.clip)}"]`)?.closest('[data-track-lane]')
129
+ : ruler;
130
+ const row = rectIn(lane);
131
+ return row ? { x: row.x + (at.time * px) / 1000 - 3, y: row.y, width: 6, height: row.height } : null;
132
+ },
133
+ bounds: () => {
134
+ const r = container?.getBoundingClientRect();
135
+ return r && r.width > 0 ? { x: 0, y: 0, width: r.width, height: r.height } : null;
136
+ },
137
+ visible: () => {
138
+ const r = container?.getBoundingClientRect();
139
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
140
+ },
141
+ toViewport: (p: { x: number; y: number }) => {
142
+ const r = container?.getBoundingClientRect();
143
+ return { x: (r?.left ?? 0) + p.x, y: (r?.top ?? 0) + p.y };
144
+ },
145
+ fromViewport: (p: { x: number; y: number }) => {
146
+ const r = container?.getBoundingClientRect();
147
+ return { x: p.x - (r?.left ?? 0), y: p.y - (r?.top ?? 0) };
148
+ },
149
+ reveal: () => onplaybackreveal?.()
150
+ }));
93
151
  let ruler: HTMLDivElement | undefined = $state();
94
152
 
95
153
  interface Drag {
@@ -262,7 +320,7 @@
262
320
  <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
263
321
  <div
264
322
  bind:this={container}
265
- class="flex flex-col gap-1 text-xs outline-none focus-visible:ring-1 focus-visible:ring-rose-500 {className}"
323
+ class="relative flex flex-col gap-1 text-xs outline-none focus-visible:ring-1 focus-visible:ring-rose-500 {className}"
266
324
  tabindex="0"
267
325
  role="group"
268
326
  aria-label="Timeline"
@@ -346,6 +404,7 @@
346
404
  class="relative shrink-0 {track.locked ? 'bg-muted/60' : ''} {track.hidden || track.muted ? 'opacity-60' : ''}"
347
405
  style="width:{width}px"
348
406
  data-lane={track.id}
407
+ data-track-lane={track.id}
349
408
  onpointerdown={(e) => {
350
409
  if (e.button === 0 && !(e.shiftKey || e.metaKey || e.ctrlKey)) controller.select(null);
351
410
  }}
@@ -394,4 +453,7 @@
394
453
  {#if editor.error}
395
454
  <p class="text-red-500" role="alert" data-edit-error="">{editor.error}</p>
396
455
  {/if}
456
+ {#if playing}
457
+ <PresenceLayer presence={agentPresence} />
458
+ {/if}
397
459
  </div>
@@ -1,4 +1,5 @@
1
1
  import { type VideoEditorController, type VideoWaveformMode } from '@pixerate/editor/video-editor';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  interface Props {
3
4
  controller: VideoEditorController;
4
5
  /** Zoom: pixels per second of timeline (default 100). Bindable. */
@@ -10,6 +11,15 @@ interface Props {
10
11
  /** Draw waveforms in audio clips (default), in every clip with sound (`all`), or not at all. Sound is decoded once per asset through `controller.audioPeaks`. */
11
12
  waveforms?: VideoWaveformMode;
12
13
  class?: string;
14
+ /**
15
+ * Agent playback: act out the video tools' calls here with the agent's cursor (default:
16
+ * `setAgentPlayback` above; `null` turns it off).
17
+ */
18
+ playback?: AgentPlayback | null;
19
+ /** The playback surface id (default `video`, where the video tool providers play). */
20
+ playbackId?: string;
21
+ /** Bring the timeline on screen (switch to its tab) before the agent acts. */
22
+ onplaybackreveal?: () => void | Promise<void>;
13
23
  }
14
24
  declare const VideoTimeline: import("svelte").Component<Props, {}, "pxPerSecond">;
15
25
  type VideoTimeline = ReturnType<typeof VideoTimeline>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.102.0",
3
+ "version": "0.103.2",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -144,7 +144,7 @@
144
144
  "@tiptap/extension-bubble-menu": "^2.27.3",
145
145
  "@tiptap/extension-placeholder": "^2.27.3",
146
146
  "@tiptap/starter-kit": "^2.27.3",
147
- "@pixerate/editor": "0.141.0"
147
+ "@pixerate/editor": "0.143.0"
148
148
  },
149
149
  "peerDependencies": {
150
150
  "@dagrejs/dagre": "^1.1.4",