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