@pixerate/editor-svelte 0.152.0 → 0.154.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/image-editor/ContentAwareFillWorkspace.svelte +266 -0
- package/dist/image-editor/ContentAwareFillWorkspace.svelte.d.ts +20 -0
- package/dist/image-editor/RefineEdgePanel.svelte +7 -3
- package/dist/image-editor/SelectAndMaskWorkspace.svelte +8 -3
- package/dist/image-editor/SelectionOptionsBar.svelte +6 -0
- package/dist/image-editor/index.d.ts +3 -0
- package/dist/image-editor/index.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
3
|
+
import ImageRange from './controls/ImageRange.svelte';
|
|
4
|
+
import ImageSelect from './controls/ImageSelect.svelte';
|
|
5
|
+
import { imageSectionTitleClass } from './styles.js';
|
|
6
|
+
import type { ContentAwareFillSettings, CropBox, ImageEditorController } from '@pixerate/editor/image-editor';
|
|
7
|
+
import { CONTENT_AWARE_FILL_SAMPLING, CONTENT_AWARE_FILL_TOOLS, ContentAwareFillSession } from '@pixerate/editor/image-editor';
|
|
8
|
+
import { untrack } from 'svelte';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Content-Aware Fill: a full-window workspace with the selection's surroundings on the left, the
|
|
12
|
+
* sampling area shown in green and painted with the Sampling Brush (Alt subtracts), and a live
|
|
13
|
+
* preview of the fill on the right. OK fills as one undo step, the same fill the
|
|
14
|
+
* `content_aware_fill` tool makes; Cancel and Escape change nothing.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
controller: ImageEditorController;
|
|
18
|
+
/** Called with the result after OK, or `null` after Cancel. */
|
|
19
|
+
onclose: (result: { layerId: string; bounds: CropBox; output: 'current' | 'new-layer' } | null) => void;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { controller, onclose, class: className = '' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const session = untrack(() => new ContentAwareFillSession(controller));
|
|
26
|
+
let tick = $state(0);
|
|
27
|
+
let zoom = $state(1);
|
|
28
|
+
let pan = $state({ x: 0, y: 0 });
|
|
29
|
+
let busy = $state(false);
|
|
30
|
+
let error = $state<string | null>(null);
|
|
31
|
+
let source: HTMLCanvasElement | undefined = $state();
|
|
32
|
+
let preview: HTMLCanvasElement | undefined = $state();
|
|
33
|
+
let panFrom: { x: number; y: number; pan: { x: number; y: number } } | null = null;
|
|
34
|
+
|
|
35
|
+
// Untracked: opening emits before its first await, and `tick++` reads `tick`.
|
|
36
|
+
$effect(() =>
|
|
37
|
+
untrack(() => {
|
|
38
|
+
const off = session.subscribe(() => tick++);
|
|
39
|
+
void session.open();
|
|
40
|
+
return off;
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const settings = $derived.by(() => {
|
|
45
|
+
void tick;
|
|
46
|
+
return session.settings;
|
|
47
|
+
});
|
|
48
|
+
const win = $derived.by(() => {
|
|
49
|
+
void tick;
|
|
50
|
+
return session.window;
|
|
51
|
+
});
|
|
52
|
+
const computing = $derived.by(() => {
|
|
53
|
+
void tick;
|
|
54
|
+
return session.computing;
|
|
55
|
+
});
|
|
56
|
+
const sessionError = $derived.by(() => {
|
|
57
|
+
void tick;
|
|
58
|
+
return session.error;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Draw both views whenever the session changes.
|
|
62
|
+
$effect(() => {
|
|
63
|
+
void tick;
|
|
64
|
+
if (!win || typeof ImageData === 'undefined') return;
|
|
65
|
+
for (const [canvas, rgba] of [
|
|
66
|
+
[source, session.renderSource()],
|
|
67
|
+
[preview, session.renderPreview()],
|
|
68
|
+
] as const) {
|
|
69
|
+
const ctx = canvas?.getContext?.('2d');
|
|
70
|
+
if (!canvas || !ctx || !rgba.length) continue;
|
|
71
|
+
canvas.width = win.width;
|
|
72
|
+
canvas.height = win.height;
|
|
73
|
+
ctx.putImageData(new ImageData(rgba as Uint8ClampedArray<ArrayBuffer>, win.width, win.height), 0, 0);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const set = (changes: Partial<ContentAwareFillSettings>) => session.update(changes);
|
|
78
|
+
|
|
79
|
+
async function ok() {
|
|
80
|
+
busy = true;
|
|
81
|
+
error = null;
|
|
82
|
+
try {
|
|
83
|
+
onclose(await session.commit());
|
|
84
|
+
} catch (err) {
|
|
85
|
+
error = err instanceof Error ? err.message : String(err);
|
|
86
|
+
busy = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onkeydown(e: KeyboardEvent) {
|
|
91
|
+
const t = e.target as HTMLElement | null;
|
|
92
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'SELECT' || t.tagName === 'TEXTAREA')) return;
|
|
93
|
+
if (e.key === 'Escape') onclose(null);
|
|
94
|
+
else if (e.key === 'Enter') void ok();
|
|
95
|
+
else if (e.ctrlKey || e.metaKey || e.altKey || !session.handleKey(e.key)) return;
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The pointer's position in document pixels on the source view. */
|
|
100
|
+
function toImage(e: PointerEvent) {
|
|
101
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
102
|
+
const w = win ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
103
|
+
return { x: w.x + ((e.clientX - rect.left) / Math.max(1, rect.width)) * w.width, y: w.y + ((e.clientY - rect.top) / Math.max(1, rect.height)) * w.height };
|
|
104
|
+
}
|
|
105
|
+
function onpointerdown(e: PointerEvent) {
|
|
106
|
+
if (settings.tool === 'hand') {
|
|
107
|
+
panFrom = { x: e.clientX, y: e.clientY, pan };
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (settings.tool === 'zoom') {
|
|
111
|
+
zoom = Math.min(16, Math.max(0.25, e.altKey ? zoom / 1.25 : zoom * 1.25));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
|
115
|
+
session.begin(toImage(e), { altKey: e.altKey });
|
|
116
|
+
}
|
|
117
|
+
function onpointermove(e: PointerEvent) {
|
|
118
|
+
if (panFrom) {
|
|
119
|
+
pan = { x: panFrom.pan.x + e.clientX - panFrom.x, y: panFrom.pan.y + e.clientY - panFrom.y };
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (e.buttons) session.move(toImage(e));
|
|
123
|
+
}
|
|
124
|
+
function onpointerup() {
|
|
125
|
+
panFrom = null;
|
|
126
|
+
session.end();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const button =
|
|
130
|
+
'px-2 py-1 rounded-md border border-border bg-background text-[11px] text-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40';
|
|
131
|
+
const toggle = (on: boolean) => `${button} ${on ? 'bg-primary text-primary-foreground border-primary' : ''}`;
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<svelte:window {onkeydown} />
|
|
135
|
+
|
|
136
|
+
<div data-testid="content-aware-fill-workspace" role="dialog" aria-label="Content-Aware Fill" class="fixed inset-0 z-50 flex bg-background text-xs text-foreground {className}">
|
|
137
|
+
<div role="group" aria-label="Content-Aware Fill tool" class="flex flex-col gap-1 p-2 border-r border-border">
|
|
138
|
+
{#each CONTENT_AWARE_FILL_TOOLS as t (t.id)}
|
|
139
|
+
<button type="button" title="{t.label} ({t.key.toUpperCase()})" aria-pressed={settings.tool === t.id} class={toggle(settings.tool === t.id)} onclick={() => set({ tool: t.id })}>
|
|
140
|
+
{t.label}
|
|
141
|
+
</button>
|
|
142
|
+
{/each}
|
|
143
|
+
</div>
|
|
144
|
+
<div class="flex-1 flex flex-col min-w-0">
|
|
145
|
+
<div class="flex flex-wrap items-center gap-2 p-2 border-b border-border">
|
|
146
|
+
{#if settings.tool === 'sampling-brush'}
|
|
147
|
+
<label class="flex items-center gap-1 text-muted-foreground">
|
|
148
|
+
Size
|
|
149
|
+
<input
|
|
150
|
+
type="number"
|
|
151
|
+
min="1"
|
|
152
|
+
aria-label="Sampling brush size"
|
|
153
|
+
value={settings.brushSize}
|
|
154
|
+
oninput={(e) => set({ brushSize: Math.max(1, Number((e.currentTarget as HTMLInputElement).value) || 1) })}
|
|
155
|
+
class="w-16 bg-background border border-border rounded px-1 py-0.5 text-foreground"
|
|
156
|
+
/>
|
|
157
|
+
px
|
|
158
|
+
</label>
|
|
159
|
+
<div role="group" aria-label="Sampling brush mode" class="flex gap-1">
|
|
160
|
+
<button type="button" aria-pressed={settings.brushMode === 'add'} class={toggle(settings.brushMode === 'add')} onclick={() => set({ brushMode: 'add' })}>Add</button>
|
|
161
|
+
<button type="button" aria-pressed={settings.brushMode === 'subtract'} class={toggle(settings.brushMode === 'subtract')} onclick={() => set({ brushMode: 'subtract' })}>
|
|
162
|
+
Subtract
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
{/if}
|
|
166
|
+
<span class="font-mono text-muted-foreground">{Math.round(zoom * 100)}%</span>
|
|
167
|
+
{#if computing}
|
|
168
|
+
<span role="status" class="text-muted-foreground">Updating preview…</span>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
<div class="flex-1 grid grid-cols-2 gap-2 p-2 min-h-0">
|
|
172
|
+
<div class="flex flex-col min-h-0">
|
|
173
|
+
<span class="text-muted-foreground">Source (green: sampling area)</span>
|
|
174
|
+
<div class="flex-1 overflow-hidden flex items-center justify-center bg-muted/30">
|
|
175
|
+
<canvas
|
|
176
|
+
bind:this={source}
|
|
177
|
+
data-testid="content-aware-fill-source"
|
|
178
|
+
{onpointerdown}
|
|
179
|
+
{onpointermove}
|
|
180
|
+
{onpointerup}
|
|
181
|
+
style:max-width="100%"
|
|
182
|
+
style:max-height="100%"
|
|
183
|
+
style:transform="translate({pan.x}px, {pan.y}px) scale({zoom})"
|
|
184
|
+
style:image-rendering={zoom > 2 ? 'pixelated' : undefined}
|
|
185
|
+
style:cursor={settings.tool === 'hand' ? 'grab' : settings.tool === 'zoom' ? 'zoom-in' : 'crosshair'}
|
|
186
|
+
></canvas>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="flex flex-col min-h-0">
|
|
190
|
+
<span class="text-muted-foreground">Preview</span>
|
|
191
|
+
<div class="flex-1 overflow-hidden flex items-center justify-center bg-muted/30">
|
|
192
|
+
<canvas
|
|
193
|
+
bind:this={preview}
|
|
194
|
+
data-testid="content-aware-fill-preview"
|
|
195
|
+
style:max-width="100%"
|
|
196
|
+
style:max-height="100%"
|
|
197
|
+
style:transform="translate({pan.x}px, {pan.y}px) scale({zoom})"
|
|
198
|
+
style:image-rendering={zoom > 2 ? 'pixelated' : undefined}
|
|
199
|
+
></canvas>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="w-64 flex flex-col gap-3 p-3 border-l border-border overflow-y-auto">
|
|
205
|
+
<span class={imageSectionTitleClass}>Sampling Area Options</span>
|
|
206
|
+
<div role="group" aria-label="Sampling area" class="flex gap-1">
|
|
207
|
+
{#each CONTENT_AWARE_FILL_SAMPLING as m (m.id)}
|
|
208
|
+
<button type="button" aria-pressed={settings.sampling === m.id} class={toggle(settings.sampling === m.id)} onclick={() => set({ sampling: m.id })}>{m.label}</button>
|
|
209
|
+
{/each}
|
|
210
|
+
</div>
|
|
211
|
+
<label class="flex items-center gap-2">
|
|
212
|
+
<ImageCheckbox label="Show Sampling Area" checked={settings.showSampling} onchange={(on) => set({ showSampling: on })} />
|
|
213
|
+
Show Sampling Area
|
|
214
|
+
</label>
|
|
215
|
+
<label class="flex flex-col gap-1">
|
|
216
|
+
<span class="flex justify-between text-muted-foreground">
|
|
217
|
+
<span>Opacity</span>
|
|
218
|
+
<span class="font-mono">{Math.round(settings.opacity * 100)}%</span>
|
|
219
|
+
</span>
|
|
220
|
+
<ImageRange label="Sampling overlay opacity" min={0} max={100} step={1} value={Math.round(settings.opacity * 100)} oninput={(v) => set({ opacity: v / 100 })} />
|
|
221
|
+
</label>
|
|
222
|
+
|
|
223
|
+
<span class={imageSectionTitleClass}>Fill Settings</span>
|
|
224
|
+
<label class="flex items-center gap-2 text-muted-foreground">
|
|
225
|
+
Seed
|
|
226
|
+
<input
|
|
227
|
+
type="number"
|
|
228
|
+
min="0"
|
|
229
|
+
aria-label="Fill seed"
|
|
230
|
+
value={settings.seed}
|
|
231
|
+
oninput={(e) => set({ seed: Math.max(0, Math.round(Number((e.currentTarget as HTMLInputElement).value) || 0)) })}
|
|
232
|
+
class="w-16 bg-background border border-border rounded px-1 py-0.5 text-foreground"
|
|
233
|
+
/>
|
|
234
|
+
<button type="button" class={button} onclick={() => set({ seed: settings.seed + 1 })}>Try Another</button>
|
|
235
|
+
</label>
|
|
236
|
+
<label class="flex items-center gap-2">
|
|
237
|
+
<ImageCheckbox label="Live Preview" checked={settings.livePreview} onchange={(on) => set({ livePreview: on })} />
|
|
238
|
+
Live Preview
|
|
239
|
+
</label>
|
|
240
|
+
{#if !settings.livePreview}
|
|
241
|
+
<button type="button" class={button} disabled={computing} onclick={() => void session.refresh()}>Preview</button>
|
|
242
|
+
{/if}
|
|
243
|
+
|
|
244
|
+
<span class={imageSectionTitleClass}>Output Settings</span>
|
|
245
|
+
<label class="flex items-center gap-2">
|
|
246
|
+
Output To
|
|
247
|
+
<ImageSelect
|
|
248
|
+
label="Fill output"
|
|
249
|
+
value={settings.output}
|
|
250
|
+
options={[
|
|
251
|
+
{ value: 'new-layer', label: 'New Layer' },
|
|
252
|
+
{ value: 'current', label: 'Current Layer' },
|
|
253
|
+
]}
|
|
254
|
+
onchange={(v) => set({ output: v as 'current' | 'new-layer' })}
|
|
255
|
+
/>
|
|
256
|
+
</label>
|
|
257
|
+
{#if error ?? sessionError}
|
|
258
|
+
<p role="alert" class="text-destructive">{error ?? sessionError}</p>
|
|
259
|
+
{/if}
|
|
260
|
+
<div class="flex gap-1 mt-auto">
|
|
261
|
+
<button type="button" class={button} onclick={() => session.reset()}>Reset</button>
|
|
262
|
+
<button type="button" class={button} onclick={() => onclose(null)}>Cancel</button>
|
|
263
|
+
<button type="button" class={toggle(true)} disabled={busy || !win} aria-busy={busy} onclick={() => void ok()}>OK</button>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CropBox, ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
|
+
/**
|
|
3
|
+
* Content-Aware Fill: a full-window workspace with the selection's surroundings on the left, the
|
|
4
|
+
* sampling area shown in green and painted with the Sampling Brush (Alt subtracts), and a live
|
|
5
|
+
* preview of the fill on the right. OK fills as one undo step, the same fill the
|
|
6
|
+
* `content_aware_fill` tool makes; Cancel and Escape change nothing.
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
controller: ImageEditorController;
|
|
10
|
+
/** Called with the result after OK, or `null` after Cancel. */
|
|
11
|
+
onclose: (result: {
|
|
12
|
+
layerId: string;
|
|
13
|
+
bounds: CropBox;
|
|
14
|
+
output: 'current' | 'new-layer';
|
|
15
|
+
} | null) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const ContentAwareFillWorkspace: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type ContentAwareFillWorkspace = ReturnType<typeof ContentAwareFillWorkspace>;
|
|
20
|
+
export default ContentAwareFillWorkspace;
|
|
@@ -55,7 +55,11 @@
|
|
|
55
55
|
void tick;
|
|
56
56
|
return Boolean(controller.segmentationProvider);
|
|
57
57
|
});
|
|
58
|
-
const toLayer = $derived(output
|
|
58
|
+
const toLayer = $derived(output !== 'selection' && output !== 'layer-mask');
|
|
59
|
+
const outputs = $derived.by(() => {
|
|
60
|
+
void tick;
|
|
61
|
+
return REFINE_OUTPUTS.filter((o) => !o.needsDocuments || controller.canOpenDocuments);
|
|
62
|
+
});
|
|
59
63
|
const target = $derived(list.find((l) => l.id === layerId) ?? list[list.length - 1]);
|
|
60
64
|
|
|
61
65
|
async function refine() {
|
|
@@ -124,14 +128,14 @@
|
|
|
124
128
|
onchange={(e) => (output = (e.currentTarget as HTMLSelectElement).value as RefineOutput)}
|
|
125
129
|
class="flex-1 bg-background border border-border rounded px-1 py-1 text-foreground"
|
|
126
130
|
>
|
|
127
|
-
{#each
|
|
131
|
+
{#each outputs as o (o.id)}
|
|
128
132
|
<option value={o.id}>{o.label}</option>
|
|
129
133
|
{/each}
|
|
130
134
|
</select>
|
|
131
135
|
</label>
|
|
132
136
|
<label class="flex flex-col gap-1">
|
|
133
137
|
<span class="flex justify-between text-muted-foreground">
|
|
134
|
-
<span>Decontaminate colors{toLayer ? '' : ' (new layer outputs)'}</span>
|
|
138
|
+
<span>Decontaminate colors{toLayer ? '' : ' (new layer or document outputs)'}</span>
|
|
135
139
|
<span class="font-mono">{decontaminate}%</span>
|
|
136
140
|
</span>
|
|
137
141
|
<input
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
session.end();
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
const toLayer = $derived(settings.output
|
|
130
|
+
const toLayer = $derived(settings.output !== 'selection' && settings.output !== 'layer-mask');
|
|
131
131
|
const brushTool = $derived(settings.tool === 'quick-select' || settings.tool === 'brush' || settings.tool === 'refine-brush');
|
|
132
132
|
|
|
133
133
|
const button =
|
|
@@ -258,14 +258,19 @@
|
|
|
258
258
|
<span class={imageSectionTitleClass}>Output Settings</span>
|
|
259
259
|
<label class="flex flex-col gap-1">
|
|
260
260
|
<span class="flex justify-between text-muted-foreground">
|
|
261
|
-
<span>Decontaminate Colors{toLayer ? '' : ' (new layer outputs)'}</span>
|
|
261
|
+
<span>Decontaminate Colors{toLayer ? '' : ' (new layer or document outputs)'}</span>
|
|
262
262
|
<span class="font-mono">{settings.decontaminate}%</span>
|
|
263
263
|
</span>
|
|
264
264
|
<ImageRange label="Workspace decontaminate amount" min={0} max={100} step={1} value={settings.decontaminate} disabled={!toLayer} oninput={(v) => set({ decontaminate: v })} />
|
|
265
265
|
</label>
|
|
266
266
|
<label class="flex items-center gap-2">
|
|
267
267
|
Output To
|
|
268
|
-
<ImageSelect
|
|
268
|
+
<ImageSelect
|
|
269
|
+
label="Output to"
|
|
270
|
+
value={settings.output}
|
|
271
|
+
options={REFINE_OUTPUTS.filter((o) => !o.needsDocuments || controller.canOpenDocuments).map((o) => ({ value: o.id, label: o.label }))}
|
|
272
|
+
onchange={(v) => set({ output: v as RefineOutput })}
|
|
273
|
+
/>
|
|
269
274
|
</label>
|
|
270
275
|
<label class="flex items-center gap-2">
|
|
271
276
|
<ImageCheckbox label="Remember Settings" checked={settings.remember} onchange={(on) => set({ remember: on })} />
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import ImageRange from './controls/ImageRange.svelte';
|
|
4
4
|
import ImageSelect from './controls/ImageSelect.svelte';
|
|
5
5
|
import SelectAndMaskWorkspace from './SelectAndMaskWorkspace.svelte';
|
|
6
|
+
import ContentAwareFillWorkspace from './ContentAwareFillWorkspace.svelte';
|
|
6
7
|
import type {
|
|
7
8
|
ColorRangePreset,
|
|
8
9
|
ImageEditorController,
|
|
@@ -125,6 +126,7 @@
|
|
|
125
126
|
let segmenting = $state(false);
|
|
126
127
|
let subjectStatus = $state<string | null>(null);
|
|
127
128
|
let workspace = $state(false);
|
|
129
|
+
let fillWorkspace = $state(false);
|
|
128
130
|
let skying = $state(false);
|
|
129
131
|
let skyStatus = $state<string | null>(null);
|
|
130
132
|
async function selectSky() {
|
|
@@ -570,6 +572,10 @@
|
|
|
570
572
|
<button type="button" class={btn(false)} disabled={!sel} onclick={() => edit(() => controller.strokeSelection({ width: strokeWidth, location: strokeLocation }))}>Stroke</button>
|
|
571
573
|
<button type="button" class={btn(false)} disabled={!sel} onclick={() => edit(() => controller.cropToSelection())}>Crop to Selection</button>
|
|
572
574
|
<button type="button" class={btn(false)} disabled={!sel} onclick={() => edit(() => controller.contentAwareFill({ output: 'new-layer' }))}>Content-Aware Fill</button>
|
|
575
|
+
<button type="button" class={btn(false)} disabled={!sel} onclick={() => (fillWorkspace = true)}>Content-Aware Fill…</button>
|
|
576
|
+
{#if fillWorkspace}
|
|
577
|
+
<ContentAwareFillWorkspace {controller} onclose={() => (fillWorkspace = false)} />
|
|
578
|
+
{/if}
|
|
573
579
|
</div>
|
|
574
580
|
<div role="group" aria-label="Clipboard" class="flex flex-wrap gap-1">
|
|
575
581
|
<button type="button" class={btn(false)} onclick={() => edit(() => controller.copyPixels())}>Copy</button>
|
|
@@ -9,6 +9,7 @@ import type ControlSliderComponent from "./ControlSlider.svelte";
|
|
|
9
9
|
import type SelectionOptionsBarComponent from "./SelectionOptionsBar.svelte";
|
|
10
10
|
import type RefineEdgePanelComponent from "./RefineEdgePanel.svelte";
|
|
11
11
|
import type SelectAndMaskWorkspaceComponent from "./SelectAndMaskWorkspace.svelte";
|
|
12
|
+
import type ContentAwareFillWorkspaceComponent from "./ContentAwareFillWorkspace.svelte";
|
|
12
13
|
import type ChannelsPanelComponent from "./ChannelsPanel.svelte";
|
|
13
14
|
import type LayersPanelComponent from "./LayersPanel.svelte";
|
|
14
15
|
import type ColorProfilePanelComponent from "./ColorProfilePanel.svelte";
|
|
@@ -34,6 +35,7 @@ export { default as ControlSlider } from './ControlSlider.svelte';
|
|
|
34
35
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
35
36
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
36
37
|
export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
|
|
38
|
+
export { default as ContentAwareFillWorkspace } from './ContentAwareFillWorkspace.svelte';
|
|
37
39
|
export { default as ChannelsPanel } from './ChannelsPanel.svelte';
|
|
38
40
|
export { default as LayersPanel } from './LayersPanel.svelte';
|
|
39
41
|
export { default as ColorProfilePanel } from './ColorProfilePanel.svelte';
|
|
@@ -61,6 +63,7 @@ export type ControlSliderProps = ComponentProps<typeof ControlSliderComponent>;
|
|
|
61
63
|
export type SelectionOptionsBarProps = ComponentProps<typeof SelectionOptionsBarComponent>;
|
|
62
64
|
export type RefineEdgePanelProps = ComponentProps<typeof RefineEdgePanelComponent>;
|
|
63
65
|
export type SelectAndMaskWorkspaceProps = ComponentProps<typeof SelectAndMaskWorkspaceComponent>;
|
|
66
|
+
export type ContentAwareFillWorkspaceProps = ComponentProps<typeof ContentAwareFillWorkspaceComponent>;
|
|
64
67
|
export type ChannelsPanelProps = ComponentProps<typeof ChannelsPanelComponent>;
|
|
65
68
|
export type LayersPanelProps = ComponentProps<typeof LayersPanelComponent>;
|
|
66
69
|
export type ColorProfilePanelProps = ComponentProps<typeof ColorProfilePanelComponent>;
|
|
@@ -11,6 +11,7 @@ export { default as ControlSlider } from './ControlSlider.svelte';
|
|
|
11
11
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
12
12
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
13
13
|
export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
|
|
14
|
+
export { default as ContentAwareFillWorkspace } from './ContentAwareFillWorkspace.svelte';
|
|
14
15
|
export { default as ChannelsPanel } from './ChannelsPanel.svelte';
|
|
15
16
|
export { default as LayersPanel } from './LayersPanel.svelte';
|
|
16
17
|
export { default as ColorProfilePanel } from './ColorProfilePanel.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.154.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"**/*.css"
|
|
166
166
|
],
|
|
167
167
|
"dependencies": {
|
|
168
|
-
"@pixerate/editor": "0.
|
|
168
|
+
"@pixerate/editor": "0.210.0"
|
|
169
169
|
},
|
|
170
170
|
"peerDependencies": {
|
|
171
171
|
"@dagrejs/dagre": "^1.1.4",
|