@pixerate/editor-svelte 0.102.0 → 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.
- package/dist/animation-editor/AnimationEditor.svelte +35 -3
- package/dist/animation-editor/AnimationEditor.svelte.d.ts +10 -0
- package/dist/chart/ChartEditor.svelte +85 -4
- package/dist/chart/ChartEditor.svelte.d.ts +11 -0
- package/dist/paint-editor/PaintEditor.svelte +92 -10
- package/dist/paint-editor/PaintEditor.svelte.d.ts +12 -1
- package/dist/parameters/ParametersPanel.svelte +31 -2
- package/dist/parameters/ParametersPanel.svelte.d.ts +10 -0
- package/dist/playback/index.d.ts +1 -1
- package/dist/playback/index.js +1 -1
- package/dist/playback/playback.svelte.d.ts +17 -1
- package/dist/playback/playback.svelte.js +41 -2
- package/dist/spreadsheet/SpreadsheetEditor.svelte +63 -3
- package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +10 -0
- package/dist/video-editor/VideoEditor.svelte +8 -3
- package/dist/video-editor/VideoEditor.svelte.d.ts +5 -0
- package/dist/video-editor/VideoTimeline.svelte +64 -2
- package/dist/video-editor/VideoTimeline.svelte.d.ts +10 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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>;
|
package/dist/playback/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/playback/index.js
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.103.1",
|
|
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.
|
|
147
|
+
"@pixerate/editor": "0.142.1"
|
|
148
148
|
},
|
|
149
149
|
"peerDependencies": {
|
|
150
150
|
"@dagrejs/dagre": "^1.1.4",
|