@pixerate/editor-svelte 0.101.3 → 0.102.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentTools.svelte.d.ts +4 -2
- package/dist/agentTools.svelte.js +8 -1
- 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/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/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 +16 -0
- package/dist/playback/playback.svelte.js +52 -0
- package/dist/vector/VectorCanvas.svelte +54 -1
- package/dist/vector/VectorCanvas.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);
|
|
@@ -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';
|
|
@@ -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";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import type { PresenceStore, PresenceState } from '@pixerate/editor/tools';
|
|
4
|
+
|
|
5
|
+
type Point = { x: number; y: number };
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** The cursors to draw, e.g. from `createPlaybackSurface` or a director. */
|
|
9
|
+
presence: PresenceStore;
|
|
10
|
+
/**
|
|
11
|
+
* How surface coordinates map to this layer's pixels: a canvas viewport
|
|
12
|
+
* (`x * zoom + viewport.x`), or a function. Default: one to one.
|
|
13
|
+
*/
|
|
14
|
+
viewport?: { x: number; y: number; zoom: number } | ((point: Point) => Point);
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { presence, viewport, class: className = '' }: Props = $props();
|
|
19
|
+
let view: PresenceState = $state.raw(untrack(() => presence.getState()));
|
|
20
|
+
$effect(() => {
|
|
21
|
+
view = presence.getState();
|
|
22
|
+
return presence.subscribe((next) => {
|
|
23
|
+
view = next;
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const map = $derived.by((): ((p: Point) => Point) => {
|
|
28
|
+
const v = viewport;
|
|
29
|
+
if (typeof v === 'function') return v;
|
|
30
|
+
if (v) return (p) => ({ x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y });
|
|
31
|
+
return (p) => p;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function ring(focus: { x: number; y: number; width: number; height: number }) {
|
|
35
|
+
const tl = map(focus);
|
|
36
|
+
const br = map({ x: focus.x + focus.width, y: focus.y + focus.height });
|
|
37
|
+
return `left: ${tl.x - 4}px; top: ${tl.y - 4}px; width: ${br.x - tl.x + 8}px; height: ${br.y - tl.y + 8}px;`;
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<!--
|
|
42
|
+
Agent cursors with their name tag or note, and a ring around what each one works on. Place it in
|
|
43
|
+
a `position: relative` box over the surface. The `pixerate-presence` class keeps it out of
|
|
44
|
+
snapshots and exports. Parity with React's `<PresenceLayer>`.
|
|
45
|
+
-->
|
|
46
|
+
{#if view.actors.length}
|
|
47
|
+
<div class="pixerate-presence {className}" aria-hidden="true" data-testid="presence-layer" style="position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 50;">
|
|
48
|
+
{#each view.actors as a (a.id)}
|
|
49
|
+
{#if a.focus}
|
|
50
|
+
<div data-actor-focus={a.id} style="position: absolute; {ring(a.focus)} border-radius: 8px; border: 2px solid {a.color}; background: {a.color}14; transition: all 160ms ease-out;"></div>
|
|
51
|
+
{/if}
|
|
52
|
+
<div data-actor={a.id} style="position: absolute; left: 0; top: 0; transform: translate({map(a).x}px, {map(a).y}px);">
|
|
53
|
+
<svg width="18" height="20" viewBox="0 0 12 18" style="display: block; transform-origin: 0 0; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); {a.pressed ? 'transform: scale(0.85);' : ''}">
|
|
54
|
+
<path d="M0,0 L0,14 L4,10.5 L7,17 L9.5,16 L6.5,9.5 L11.5,9.5 Z" style="fill: {a.color}; stroke: #ffffff; stroke-width: 1;" />
|
|
55
|
+
</svg>
|
|
56
|
+
<span style="position: absolute; left: 14px; top: 16px; display: flex; flex-direction: column; align-items: flex-start; gap: 4px;">
|
|
57
|
+
<span style="white-space: nowrap; border-radius: 999px; padding: 1px 7px; font-size: 11px; font-weight: 600; color: #ffffff; background: {a.color};">{a.name}</span>
|
|
58
|
+
{#if a.message}
|
|
59
|
+
<span data-actor-message={a.id} style="width: max-content; max-width: 240px; border-radius: 8px; border-top-left-radius: 0; padding: 4px 8px; font-size: 12px; color: #1e293b; background: #ffffff; box-shadow: 0 4px 12px rgba(15,23,42,.18);">{a.message}</span>
|
|
60
|
+
{/if}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
{/if}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PresenceStore } from '@pixerate/editor/tools';
|
|
2
|
+
type Point = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
interface Props {
|
|
7
|
+
/** The cursors to draw, e.g. from `createPlaybackSurface` or a director. */
|
|
8
|
+
presence: PresenceStore;
|
|
9
|
+
/**
|
|
10
|
+
* How surface coordinates map to this layer's pixels: a canvas viewport
|
|
11
|
+
* (`x * zoom + viewport.x`), or a function. Default: one to one.
|
|
12
|
+
*/
|
|
13
|
+
viewport?: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
zoom: number;
|
|
17
|
+
} | ((point: Point) => Point);
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const PresenceLayer: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type PresenceLayer = ReturnType<typeof PresenceLayer>;
|
|
22
|
+
export default PresenceLayer;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PresenceStore, type AgentPlayback, type PlaybackSurface } from '@pixerate/editor/tools';
|
|
2
|
+
type MaybeGetter<T> = T | (() => T);
|
|
3
|
+
/** Makes `playback` the default for every editor surface in this component's subtree (call during init). */
|
|
4
|
+
export declare function setAgentPlayback(playback: AgentPlayback | null | undefined): void;
|
|
5
|
+
/** The nearest `setAgentPlayback` playback, or `null` (call during init). */
|
|
6
|
+
export declare function getAgentPlayback(): AgentPlayback | null;
|
|
7
|
+
/** A surface's behaviour; every function is read fresh on each call. */
|
|
8
|
+
export type PlaybackSurfaceOptions = Omit<PlaybackSurface, 'presence'>;
|
|
9
|
+
/**
|
|
10
|
+
* Attaches a playback surface under `id` (the tool provider's id, or its `playbackSurface`) for the
|
|
11
|
+
* calling component's lifetime (call during init), and returns its cursors to draw with
|
|
12
|
+
* `<PresenceLayer>`. `playback` defaults to `getAgentPlayback()`; with none, nothing is attached and
|
|
13
|
+
* calls run instantly. Parity with React's `usePlaybackSurface`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options?: MaybeGetter<PlaybackSurfaceOptions>): PresenceStore;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { PresenceStore } from '@pixerate/editor/tools';
|
|
3
|
+
const KEY = Symbol.for('pixerate.agentPlayback');
|
|
4
|
+
const read = (value) => (typeof value === 'function' ? value() : value);
|
|
5
|
+
/** Makes `playback` the default for every editor surface in this component's subtree (call during init). */
|
|
6
|
+
export function setAgentPlayback(playback) {
|
|
7
|
+
setContext(KEY, playback ?? null);
|
|
8
|
+
}
|
|
9
|
+
/** The nearest `setAgentPlayback` playback, or `null` (call during init). */
|
|
10
|
+
export function getAgentPlayback() {
|
|
11
|
+
return getContext(KEY) ?? null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Attaches a playback surface under `id` (the tool provider's id, or its `playbackSurface`) for the
|
|
15
|
+
* calling component's lifetime (call during init), and returns its cursors to draw with
|
|
16
|
+
* `<PresenceLayer>`. `playback` defaults to `getAgentPlayback()`; with none, nothing is attached and
|
|
17
|
+
* calls run instantly. Parity with React's `usePlaybackSurface`.
|
|
18
|
+
*/
|
|
19
|
+
export function createPlaybackSurface(playback, id, options = {}) {
|
|
20
|
+
const ambient = getAgentPlayback();
|
|
21
|
+
const presence = new PresenceStore();
|
|
22
|
+
const o = () => read(options);
|
|
23
|
+
$effect(() => {
|
|
24
|
+
const explicit = read(playback);
|
|
25
|
+
const target = explicit === undefined ? ambient : explicit;
|
|
26
|
+
const currentId = read(id);
|
|
27
|
+
if (!target || !currentId)
|
|
28
|
+
return;
|
|
29
|
+
const surface = {
|
|
30
|
+
presence,
|
|
31
|
+
locate: (ref) => o().locate?.(ref) ?? null,
|
|
32
|
+
bounds: () => o().bounds?.() ?? null,
|
|
33
|
+
visible: () => o().visible?.() ?? true,
|
|
34
|
+
toViewport: (p) => o().toViewport?.(p) ?? p,
|
|
35
|
+
fromViewport: (p) => o().fromViewport?.(p) ?? p,
|
|
36
|
+
beginGesture: (key) => o().beginGesture?.(key),
|
|
37
|
+
endGesture: () => o().endGesture?.(),
|
|
38
|
+
reveal: () => o().reveal?.(),
|
|
39
|
+
open: (ref) => o().open?.(ref),
|
|
40
|
+
get defaultKind() {
|
|
41
|
+
return o().defaultKind;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const detach = target.attach(currentId, surface);
|
|
45
|
+
return () => {
|
|
46
|
+
detach();
|
|
47
|
+
for (const a of presence.getState().actors)
|
|
48
|
+
presence.remove(a.id);
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
return presence;
|
|
52
|
+
}
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
toVectorKeyInput,
|
|
15
15
|
toVectorPointerInput,
|
|
16
16
|
vectorDiamondPath,
|
|
17
|
+
createVectorPlaybackLocator,
|
|
17
18
|
type VectorEditorController
|
|
18
19
|
} from '@pixerate/editor/vector';
|
|
20
|
+
import type { AgentPlayback } from '@pixerate/editor/tools';
|
|
21
|
+
import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
|
|
22
|
+
import PresenceLayer from '../playback/PresenceLayer.svelte';
|
|
19
23
|
import VectorGradientDefs from './VectorGradientDefs.svelte';
|
|
20
24
|
import VectorFilterDefs from './VectorFilterDefs.svelte';
|
|
21
25
|
import VectorGradientAnnotator from './VectorGradientAnnotator.svelte';
|
|
@@ -39,6 +43,15 @@
|
|
|
39
43
|
style?: string;
|
|
40
44
|
/** Accessible label (default "Vector canvas"). */
|
|
41
45
|
'aria-label'?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Agent playback: act out the vector commands' calls 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 `vector`, where the vector commands play). */
|
|
52
|
+
playbackId?: string;
|
|
53
|
+
/** Bring the canvas on screen (switch to its tab) before the agent acts. */
|
|
54
|
+
onplaybackreveal?: () => void | Promise<void>;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
let {
|
|
@@ -50,7 +63,10 @@
|
|
|
50
63
|
idPrefix = `ve${++nextId}-`,
|
|
51
64
|
class: className = '',
|
|
52
65
|
style = '',
|
|
53
|
-
'aria-label': ariaLabel = 'Vector canvas'
|
|
66
|
+
'aria-label': ariaLabel = 'Vector canvas',
|
|
67
|
+
playback,
|
|
68
|
+
playbackId = 'vector',
|
|
69
|
+
onplaybackreveal
|
|
54
70
|
}: Props = $props();
|
|
55
71
|
|
|
56
72
|
const store = vectorStateOf(() => controller);
|
|
@@ -70,6 +86,35 @@
|
|
|
70
86
|
const as = THEME.anchorSize;
|
|
71
87
|
|
|
72
88
|
let svg: SVGSVGElement | undefined = $state();
|
|
89
|
+
|
|
90
|
+
// Agent playback: the agent's cursor in document units, drawn through the viewport.
|
|
91
|
+
const ambientPlayback = getAgentPlayback();
|
|
92
|
+
const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
|
|
93
|
+
const svgBox = () => svg?.getBoundingClientRect() ?? null;
|
|
94
|
+
const presence = createPlaybackSurface(() => playing, () => playbackId, () => ({
|
|
95
|
+
defaultKind: 'vector',
|
|
96
|
+
locate: createVectorPlaybackLocator(controller),
|
|
97
|
+
bounds: () => {
|
|
98
|
+
const r = svgBox();
|
|
99
|
+
const v = controller.getState().viewport;
|
|
100
|
+
return r && r.width > 0 ? { x: -v.x / v.zoom, y: -v.y / v.zoom, width: r.width / v.zoom, height: r.height / v.zoom } : null;
|
|
101
|
+
},
|
|
102
|
+
visible: () => {
|
|
103
|
+
const r = svgBox();
|
|
104
|
+
return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
|
|
105
|
+
},
|
|
106
|
+
toViewport: (p) => {
|
|
107
|
+
const r = svgBox();
|
|
108
|
+
const v = controller.getState().viewport;
|
|
109
|
+
return { x: (r?.left ?? 0) + p.x * v.zoom + v.x, y: (r?.top ?? 0) + p.y * v.zoom + v.y };
|
|
110
|
+
},
|
|
111
|
+
fromViewport: (p) => {
|
|
112
|
+
const r = svgBox();
|
|
113
|
+
const v = controller.getState().viewport;
|
|
114
|
+
return { x: (p.x - (r?.left ?? 0) - v.x) / v.zoom, y: (p.y - (r?.top ?? 0) - v.y) / v.zoom };
|
|
115
|
+
},
|
|
116
|
+
reveal: () => onplaybackreveal?.()
|
|
117
|
+
}));
|
|
73
118
|
$effect(() => {
|
|
74
119
|
const el = svg;
|
|
75
120
|
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
@@ -611,4 +656,12 @@
|
|
|
611
656
|
{/key}
|
|
612
657
|
</foreignObject>
|
|
613
658
|
{/if}
|
|
659
|
+
{#if playing}
|
|
660
|
+
<!-- The agent's cursor (agent playback), over everything and never in the way. -->
|
|
661
|
+
<foreignObject data-vector-presence="" x="0" y="0" width="100%" height="100%" pointer-events="none" style="overflow:visible;pointer-events:none">
|
|
662
|
+
<div style="position:relative;width:100%;height:100%">
|
|
663
|
+
<PresenceLayer {presence} viewport={view.viewport} />
|
|
664
|
+
</div>
|
|
665
|
+
</foreignObject>
|
|
666
|
+
{/if}
|
|
614
667
|
</svg>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type VectorEditorController } from '@pixerate/editor/vector';
|
|
2
|
+
import type { AgentPlayback } from '@pixerate/editor/tools';
|
|
2
3
|
interface Props {
|
|
3
4
|
controller: VectorEditorController;
|
|
4
5
|
/** Fit the artboard into the canvas when it first gets a size (default `true`). */
|
|
@@ -15,6 +16,15 @@ interface Props {
|
|
|
15
16
|
style?: string;
|
|
16
17
|
/** Accessible label (default "Vector canvas"). */
|
|
17
18
|
'aria-label'?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Agent playback: act out the vector commands' calls here with the agent's cursor (default:
|
|
21
|
+
* `setAgentPlayback` above; `null` turns it off).
|
|
22
|
+
*/
|
|
23
|
+
playback?: AgentPlayback | null;
|
|
24
|
+
/** The playback surface id (default `vector`, where the vector commands play). */
|
|
25
|
+
playbackId?: string;
|
|
26
|
+
/** Bring the canvas on screen (switch to its tab) before the agent acts. */
|
|
27
|
+
onplaybackreveal?: () => void | Promise<void>;
|
|
18
28
|
}
|
|
19
29
|
declare const VectorCanvas: import("svelte").Component<Props, {}, "">;
|
|
20
30
|
type VectorCanvas = ReturnType<typeof VectorCanvas>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.0",
|
|
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.141.0"
|
|
148
148
|
},
|
|
149
149
|
"peerDependencies": {
|
|
150
150
|
"@dagrejs/dagre": "^1.1.4",
|