@pixerate/editor-svelte 0.150.0 → 0.152.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/SelectAndMaskWorkspace.svelte +286 -0
- package/dist/image-editor/SelectAndMaskWorkspace.svelte.d.ts +16 -0
- package/dist/image-editor/SelectionOptionsBar.svelte +45 -5
- 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,286 @@
|
|
|
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 { ImageEditorController, RefineOutput, RefineOutputResult, SelectAndMaskSettings, SelectAndMaskView } from '@pixerate/editor/image-editor';
|
|
7
|
+
import { REFINE_EDGE_CONTROLS, REFINE_OUTPUTS, SELECT_AND_MASK_TOOLS, SELECT_AND_MASK_VIEWS, SelectAndMaskSession } from '@pixerate/editor/image-editor';
|
|
8
|
+
import { untrack } from 'svelte';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Select and Mask: a full-window workspace over the editor with its own tools, view modes, edge
|
|
12
|
+
* and output settings, and a live preview. OK applies everything as one undo step, the same
|
|
13
|
+
* `refineSelectionTo` call an agent makes; Cancel leaves the document as it was. Keys as in
|
|
14
|
+
* Photoshop, plus Enter (OK), Escape (Cancel) and Ctrl/⌘+Z (undo the last stroke).
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
controller: ImageEditorController;
|
|
18
|
+
/** Called with the result after OK, or `null` after Cancel. */
|
|
19
|
+
onclose: (result: RefineOutputResult | null) => void;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { controller, onclose, class: className = '' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const session = untrack(() => new SelectAndMaskSession(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 canvas: HTMLCanvasElement | undefined = $state();
|
|
32
|
+
let panFrom: { x: number; y: number; pan: { x: number; y: number } } | null = null;
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const off = session.subscribe(() => tick++);
|
|
36
|
+
void session.open();
|
|
37
|
+
return off;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const settings = $derived.by(() => {
|
|
41
|
+
void tick;
|
|
42
|
+
return session.settings;
|
|
43
|
+
});
|
|
44
|
+
const size = $derived.by(() => {
|
|
45
|
+
void tick;
|
|
46
|
+
return session.previewSize;
|
|
47
|
+
});
|
|
48
|
+
const edits = $derived.by(() => {
|
|
49
|
+
void tick;
|
|
50
|
+
return session.edits.length;
|
|
51
|
+
});
|
|
52
|
+
const sessionError = $derived.by(() => {
|
|
53
|
+
void tick;
|
|
54
|
+
return session.error;
|
|
55
|
+
});
|
|
56
|
+
const note = $derived.by(() => {
|
|
57
|
+
void tick;
|
|
58
|
+
return session.note;
|
|
59
|
+
});
|
|
60
|
+
const computing = $derived.by(() => {
|
|
61
|
+
void tick;
|
|
62
|
+
return session.computing;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Draw the preview whenever it changes.
|
|
66
|
+
$effect(() => {
|
|
67
|
+
void tick;
|
|
68
|
+
const ctx = canvas?.getContext?.('2d');
|
|
69
|
+
if (!canvas || !ctx || !size.width || typeof ImageData === 'undefined') return;
|
|
70
|
+
canvas.width = size.width;
|
|
71
|
+
canvas.height = size.height;
|
|
72
|
+
ctx.putImageData(new ImageData(session.render() as Uint8ClampedArray<ArrayBuffer>, size.width, size.height), 0, 0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const set = (changes: Partial<SelectAndMaskSettings>) => session.update(changes);
|
|
76
|
+
|
|
77
|
+
async function ok() {
|
|
78
|
+
busy = true;
|
|
79
|
+
error = null;
|
|
80
|
+
try {
|
|
81
|
+
onclose(await session.commit());
|
|
82
|
+
} catch (err) {
|
|
83
|
+
error = err instanceof Error ? err.message : String(err);
|
|
84
|
+
busy = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onkeydown(e: KeyboardEvent) {
|
|
89
|
+
const t = e.target as HTMLElement | null;
|
|
90
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'SELECT' || t.tagName === 'TEXTAREA')) return;
|
|
91
|
+
if (e.key === 'Escape') onclose(null);
|
|
92
|
+
else if (e.key === 'Enter') void ok();
|
|
93
|
+
else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') session.undoEdit();
|
|
94
|
+
else if (e.ctrlKey || e.metaKey || e.altKey || !session.handleKey(e.key)) return;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The pointer's position in natural image pixels. */
|
|
99
|
+
function toImage(e: PointerEvent) {
|
|
100
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
101
|
+
const naturalW = size.width / size.scale;
|
|
102
|
+
const naturalH = size.height / size.scale;
|
|
103
|
+
return { x: ((e.clientX - rect.left) / Math.max(1, rect.width)) * naturalW, y: ((e.clientY - rect.top) / Math.max(1, rect.height)) * naturalH };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function onpointerdown(e: PointerEvent) {
|
|
107
|
+
if (settings.tool === 'hand') {
|
|
108
|
+
panFrom = { x: e.clientX, y: e.clientY, pan };
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (settings.tool === 'zoom') {
|
|
112
|
+
zoom = Math.min(16, Math.max(0.25, e.altKey ? zoom / 1.25 : zoom * 1.25));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
|
116
|
+
session.begin(toImage(e), { altKey: e.altKey });
|
|
117
|
+
}
|
|
118
|
+
function onpointermove(e: PointerEvent) {
|
|
119
|
+
if (panFrom) {
|
|
120
|
+
pan = { x: panFrom.pan.x + e.clientX - panFrom.x, y: panFrom.pan.y + e.clientY - panFrom.y };
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (e.buttons) session.move(toImage(e));
|
|
124
|
+
}
|
|
125
|
+
function onpointerup() {
|
|
126
|
+
panFrom = null;
|
|
127
|
+
session.end();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const toLayer = $derived(settings.output === 'new-layer' || settings.output === 'new-layer-with-mask');
|
|
131
|
+
const brushTool = $derived(settings.tool === 'quick-select' || settings.tool === 'brush' || settings.tool === 'refine-brush');
|
|
132
|
+
|
|
133
|
+
const button =
|
|
134
|
+
'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';
|
|
135
|
+
const toggle = (on: boolean) => `${button} ${on ? 'bg-primary text-primary-foreground border-primary' : ''}`;
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<svelte:window {onkeydown} />
|
|
139
|
+
|
|
140
|
+
<div data-testid="select-and-mask-workspace" role="dialog" aria-label="Select and Mask" class="fixed inset-0 z-50 flex bg-background text-xs text-foreground {className}">
|
|
141
|
+
<div role="group" aria-label="Select and Mask tool" class="flex flex-col gap-1 p-2 border-r border-border">
|
|
142
|
+
{#each SELECT_AND_MASK_TOOLS as t (t.id)}
|
|
143
|
+
<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 })}>
|
|
144
|
+
{t.label}
|
|
145
|
+
</button>
|
|
146
|
+
{/each}
|
|
147
|
+
</div>
|
|
148
|
+
<div class="flex-1 flex flex-col min-w-0">
|
|
149
|
+
<div class="flex flex-wrap items-center gap-2 p-2 border-b border-border">
|
|
150
|
+
{#if brushTool}
|
|
151
|
+
<label class="flex items-center gap-1 text-muted-foreground">
|
|
152
|
+
Size
|
|
153
|
+
<input
|
|
154
|
+
type="number"
|
|
155
|
+
min="1"
|
|
156
|
+
aria-label="Brush size"
|
|
157
|
+
value={settings.brushSize}
|
|
158
|
+
oninput={(e) => set({ brushSize: Math.max(1, Number((e.currentTarget as HTMLInputElement).value) || 1) })}
|
|
159
|
+
class="w-16 bg-background border border-border rounded px-1 py-0.5 text-foreground"
|
|
160
|
+
/>
|
|
161
|
+
px
|
|
162
|
+
</label>
|
|
163
|
+
{/if}
|
|
164
|
+
{#if settings.tool !== 'refine-brush' && settings.tool !== 'hand' && settings.tool !== 'zoom'}
|
|
165
|
+
<div role="group" aria-label="Stroke mode" class="flex gap-1">
|
|
166
|
+
<button type="button" aria-pressed={settings.brushMode === 'add'} class={toggle(settings.brushMode === 'add')} onclick={() => set({ brushMode: 'add' })}>Add</button>
|
|
167
|
+
<button type="button" aria-pressed={settings.brushMode === 'subtract'} class={toggle(settings.brushMode === 'subtract')} onclick={() => set({ brushMode: 'subtract' })}>
|
|
168
|
+
Subtract
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
<span class="text-muted-foreground">{edits} strokes</span>
|
|
173
|
+
<button type="button" class={button} disabled={!edits} onclick={() => session.undoEdit()}>Undo Stroke</button>
|
|
174
|
+
<span class="font-mono text-muted-foreground">{Math.round(zoom * 100)}%</span>
|
|
175
|
+
{#if computing}
|
|
176
|
+
<span role="status" class="text-muted-foreground">Updating preview…</span>
|
|
177
|
+
{/if}
|
|
178
|
+
</div>
|
|
179
|
+
<div class="flex-1 overflow-hidden flex items-center justify-center bg-muted/30">
|
|
180
|
+
<canvas
|
|
181
|
+
bind:this={canvas}
|
|
182
|
+
data-testid="select-and-mask-preview"
|
|
183
|
+
{onpointerdown}
|
|
184
|
+
{onpointermove}
|
|
185
|
+
{onpointerup}
|
|
186
|
+
style:max-width="100%"
|
|
187
|
+
style:max-height="100%"
|
|
188
|
+
style:transform="translate({pan.x}px, {pan.y}px) scale({zoom})"
|
|
189
|
+
style:image-rendering={zoom > 2 ? 'pixelated' : undefined}
|
|
190
|
+
style:cursor={settings.tool === 'hand' ? 'grab' : settings.tool === 'zoom' ? 'zoom-in' : 'crosshair'}
|
|
191
|
+
></canvas>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="w-64 flex flex-col gap-3 p-3 border-l border-border overflow-y-auto">
|
|
195
|
+
<span class={imageSectionTitleClass}>View Mode</span>
|
|
196
|
+
<label class="flex items-center gap-2">
|
|
197
|
+
View
|
|
198
|
+
<ImageSelect
|
|
199
|
+
label="View"
|
|
200
|
+
value={settings.view}
|
|
201
|
+
options={SELECT_AND_MASK_VIEWS.map((v) => ({ value: v.id, label: `${v.label} (${v.key.toUpperCase()})` }))}
|
|
202
|
+
onchange={(v) => set({ view: v as SelectAndMaskView })}
|
|
203
|
+
/>
|
|
204
|
+
</label>
|
|
205
|
+
<label class="flex flex-col gap-1">
|
|
206
|
+
<span class="flex justify-between text-muted-foreground">
|
|
207
|
+
<span>Opacity</span>
|
|
208
|
+
<span class="font-mono">{Math.round(settings.opacity * 100)}%</span>
|
|
209
|
+
</span>
|
|
210
|
+
<ImageRange label="View opacity" min={0} max={100} step={1} value={Math.round(settings.opacity * 100)} oninput={(v) => set({ opacity: v / 100 })} />
|
|
211
|
+
</label>
|
|
212
|
+
<label class="flex items-center gap-2">
|
|
213
|
+
<ImageCheckbox label="Show Edge" checked={settings.showEdge} onchange={(on) => set({ showEdge: on })} />
|
|
214
|
+
Show Edge (J)
|
|
215
|
+
</label>
|
|
216
|
+
<label class="flex items-center gap-2">
|
|
217
|
+
<ImageCheckbox label="Show Original" checked={settings.showOriginal} onchange={(on) => set({ showOriginal: on })} />
|
|
218
|
+
Show Original (P)
|
|
219
|
+
</label>
|
|
220
|
+
<label class="flex items-center gap-2">
|
|
221
|
+
<ImageCheckbox label="Real-Time Refinement" checked={settings.realTime} onchange={(on) => set({ realTime: on })} />
|
|
222
|
+
Real-Time Refinement
|
|
223
|
+
</label>
|
|
224
|
+
<label class="flex items-center gap-2">
|
|
225
|
+
<ImageCheckbox label="High Quality Preview" checked={settings.highQuality} onchange={(on) => set({ highQuality: on })} />
|
|
226
|
+
High Quality Preview
|
|
227
|
+
</label>
|
|
228
|
+
|
|
229
|
+
<span class={imageSectionTitleClass}>Edge Detection and Refinement</span>
|
|
230
|
+
{#if controller.segmentationProvider}
|
|
231
|
+
<label class="flex items-center gap-2">
|
|
232
|
+
Mode
|
|
233
|
+
<ImageSelect
|
|
234
|
+
label="Refine mode"
|
|
235
|
+
value={settings.refineMode}
|
|
236
|
+
options={[
|
|
237
|
+
{ value: 'color', label: 'Color Aware' },
|
|
238
|
+
{ value: 'object', label: 'Object Aware' },
|
|
239
|
+
]}
|
|
240
|
+
onchange={(v) => set({ refineMode: v as 'color' | 'object' })}
|
|
241
|
+
/>
|
|
242
|
+
</label>
|
|
243
|
+
{/if}
|
|
244
|
+
{#each REFINE_EDGE_CONTROLS as c (c.id)}
|
|
245
|
+
<label class="flex flex-col gap-1">
|
|
246
|
+
<span class="flex justify-between text-muted-foreground">
|
|
247
|
+
<span>{c.label}</span>
|
|
248
|
+
<span class="font-mono">{settings.params[c.id]}{c.unit}</span>
|
|
249
|
+
</span>
|
|
250
|
+
<ImageRange label="Workspace {c.label}" min={c.min} max={c.max} step={c.step} value={settings.params[c.id]} oninput={(v) => set({ params: { ...settings.params, [c.id]: v } })} />
|
|
251
|
+
</label>
|
|
252
|
+
{/each}
|
|
253
|
+
<label class="flex items-center gap-2">
|
|
254
|
+
<ImageCheckbox label="Workspace Smart Radius" checked={settings.params.smartRadius} onchange={(on) => set({ params: { ...settings.params, smartRadius: on } })} />
|
|
255
|
+
Smart Radius
|
|
256
|
+
</label>
|
|
257
|
+
|
|
258
|
+
<span class={imageSectionTitleClass}>Output Settings</span>
|
|
259
|
+
<label class="flex flex-col gap-1">
|
|
260
|
+
<span class="flex justify-between text-muted-foreground">
|
|
261
|
+
<span>Decontaminate Colors{toLayer ? '' : ' (new layer outputs)'}</span>
|
|
262
|
+
<span class="font-mono">{settings.decontaminate}%</span>
|
|
263
|
+
</span>
|
|
264
|
+
<ImageRange label="Workspace decontaminate amount" min={0} max={100} step={1} value={settings.decontaminate} disabled={!toLayer} oninput={(v) => set({ decontaminate: v })} />
|
|
265
|
+
</label>
|
|
266
|
+
<label class="flex items-center gap-2">
|
|
267
|
+
Output To
|
|
268
|
+
<ImageSelect label="Output to" value={settings.output} options={REFINE_OUTPUTS.map((o) => ({ value: o.id, label: o.label }))} onchange={(v) => set({ output: v as RefineOutput })} />
|
|
269
|
+
</label>
|
|
270
|
+
<label class="flex items-center gap-2">
|
|
271
|
+
<ImageCheckbox label="Remember Settings" checked={settings.remember} onchange={(on) => set({ remember: on })} />
|
|
272
|
+
Remember Settings
|
|
273
|
+
</label>
|
|
274
|
+
{#if error ?? sessionError}
|
|
275
|
+
<p role="alert" class="text-destructive">{error ?? sessionError}</p>
|
|
276
|
+
{/if}
|
|
277
|
+
{#if note}
|
|
278
|
+
<p class="text-muted-foreground">{note}</p>
|
|
279
|
+
{/if}
|
|
280
|
+
<div class="flex gap-1 mt-auto">
|
|
281
|
+
<button type="button" class={button} onclick={() => session.reset()}>Reset</button>
|
|
282
|
+
<button type="button" class={button} onclick={() => onclose(null)}>Cancel</button>
|
|
283
|
+
<button type="button" class={toggle(true)} disabled={busy} aria-busy={busy} onclick={() => void ok()}>OK</button>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ImageEditorController, RefineOutputResult } from '@pixerate/editor/image-editor';
|
|
2
|
+
/**
|
|
3
|
+
* Select and Mask: a full-window workspace over the editor with its own tools, view modes, edge
|
|
4
|
+
* and output settings, and a live preview. OK applies everything as one undo step, the same
|
|
5
|
+
* `refineSelectionTo` call an agent makes; Cancel leaves the document as it was. Keys as in
|
|
6
|
+
* Photoshop, plus Enter (OK), Escape (Cancel) and Ctrl/⌘+Z (undo the last stroke).
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
controller: ImageEditorController;
|
|
10
|
+
/** Called with the result after OK, or `null` after Cancel. */
|
|
11
|
+
onclose: (result: RefineOutputResult | null) => void;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const SelectAndMaskWorkspace: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type SelectAndMaskWorkspace = ReturnType<typeof SelectAndMaskWorkspace>;
|
|
16
|
+
export default SelectAndMaskWorkspace;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
3
3
|
import ImageRange from './controls/ImageRange.svelte';
|
|
4
4
|
import ImageSelect from './controls/ImageSelect.svelte';
|
|
5
|
+
import SelectAndMaskWorkspace from './SelectAndMaskWorkspace.svelte';
|
|
5
6
|
import type {
|
|
6
7
|
ColorRangePreset,
|
|
7
8
|
ImageEditorController,
|
|
@@ -106,9 +107,24 @@
|
|
|
106
107
|
void tick;
|
|
107
108
|
return Boolean(controller.segmentationProvider);
|
|
108
109
|
});
|
|
110
|
+
// The long selection running now (Select Subject, Sky, Focus Area): its progress and Cancel.
|
|
111
|
+
let task = $state<{ stage: string; fraction: number } | null>(null);
|
|
112
|
+
let taskAbort: AbortController | null = null;
|
|
113
|
+
function tracked() {
|
|
114
|
+
const abort = new AbortController();
|
|
115
|
+
taskAbort = abort;
|
|
116
|
+
task = { stage: 'Starting', fraction: 0 };
|
|
117
|
+
return { signal: abort.signal, onProgress: (fraction: number, stage: string) => (task = { stage, fraction }) };
|
|
118
|
+
}
|
|
119
|
+
function untracked() {
|
|
120
|
+
taskAbort = null;
|
|
121
|
+
task = null;
|
|
122
|
+
}
|
|
123
|
+
const failure = (err: unknown) => ((err as { name?: string })?.name === 'AbortError' ? 'Cancelled.' : err instanceof Error ? err.message : String(err));
|
|
109
124
|
let subject = $state('');
|
|
110
125
|
let segmenting = $state(false);
|
|
111
126
|
let subjectStatus = $state<string | null>(null);
|
|
127
|
+
let workspace = $state(false);
|
|
112
128
|
let skying = $state(false);
|
|
113
129
|
let skyStatus = $state<string | null>(null);
|
|
114
130
|
async function selectSky() {
|
|
@@ -116,11 +132,12 @@
|
|
|
116
132
|
skying = true;
|
|
117
133
|
skyStatus = null;
|
|
118
134
|
try {
|
|
119
|
-
const { note } = await controller.selectSky({ mode: options.mode });
|
|
135
|
+
const { note } = await controller.selectSky({ mode: options.mode, ...tracked() });
|
|
120
136
|
skyStatus = note ?? null;
|
|
121
137
|
} catch (err) {
|
|
122
|
-
skyStatus =
|
|
138
|
+
skyStatus = failure(err);
|
|
123
139
|
} finally {
|
|
140
|
+
untracked();
|
|
124
141
|
skying = false;
|
|
125
142
|
}
|
|
126
143
|
}
|
|
@@ -136,11 +153,13 @@
|
|
|
136
153
|
noise: options.focusNoise ?? 0,
|
|
137
154
|
softness: options.focusSoftness ?? 4,
|
|
138
155
|
mode: options.mode,
|
|
156
|
+
...tracked(),
|
|
139
157
|
});
|
|
140
158
|
focusStatus = selection ? null : 'No part of the image stands out as in focus.';
|
|
141
159
|
} catch (err) {
|
|
142
|
-
focusStatus =
|
|
160
|
+
focusStatus = failure(err);
|
|
143
161
|
} finally {
|
|
162
|
+
untracked();
|
|
144
163
|
focusing = false;
|
|
145
164
|
}
|
|
146
165
|
}
|
|
@@ -150,12 +169,13 @@
|
|
|
150
169
|
segmenting = true;
|
|
151
170
|
subjectStatus = null;
|
|
152
171
|
try {
|
|
153
|
-
const { segments, method, note } = await controller.selectSubject(canSegment ? target : '', { mode: options.mode });
|
|
172
|
+
const { segments, method, note } = await controller.selectSubject(canSegment ? target : '', { mode: options.mode, ...tracked() });
|
|
154
173
|
// Without a model, say only when nothing stood out.
|
|
155
174
|
subjectStatus = segments.length ? null : method === 'saliency' ? (note ?? 'Nothing stands out from the background.') : `Nothing matching “${target}” was found.`;
|
|
156
175
|
} catch (err) {
|
|
157
|
-
subjectStatus =
|
|
176
|
+
subjectStatus = failure(err);
|
|
158
177
|
} finally {
|
|
178
|
+
untracked();
|
|
159
179
|
segmenting = false;
|
|
160
180
|
}
|
|
161
181
|
}
|
|
@@ -426,6 +446,26 @@
|
|
|
426
446
|
<p role="status" class="text-muted-foreground">{subjectStatus}</p>
|
|
427
447
|
{/if}
|
|
428
448
|
</form>
|
|
449
|
+
{#if task}
|
|
450
|
+
<div data-testid="selection-progress" class="flex items-center gap-2">
|
|
451
|
+
<div
|
|
452
|
+
role="progressbar"
|
|
453
|
+
aria-label={task.stage}
|
|
454
|
+
aria-valuemin={0}
|
|
455
|
+
aria-valuemax={100}
|
|
456
|
+
aria-valuenow={Math.round(task.fraction * 100)}
|
|
457
|
+
class="flex-1 h-1.5 rounded bg-muted overflow-hidden"
|
|
458
|
+
>
|
|
459
|
+
<div class="h-full bg-primary" style:width="{Math.round(task.fraction * 100)}%"></div>
|
|
460
|
+
</div>
|
|
461
|
+
<span class="text-muted-foreground">{task.stage}</span>
|
|
462
|
+
<button type="button" class={btn(false)} onclick={() => taskAbort?.abort()}>Cancel</button>
|
|
463
|
+
</div>
|
|
464
|
+
{/if}
|
|
465
|
+
<button type="button" class={btn(false)} onclick={() => (workspace = true)}>Select and Mask…</button>
|
|
466
|
+
{#if workspace}
|
|
467
|
+
<SelectAndMaskWorkspace {controller} onclose={() => (workspace = false)} />
|
|
468
|
+
{/if}
|
|
429
469
|
<div data-testid="select-sky" class="flex flex-col gap-1">
|
|
430
470
|
<button type="button" class={btn(false)} disabled={skying} aria-busy={skying} onclick={() => void selectSky()}>
|
|
431
471
|
{skying ? 'Selecting…' : 'Select Sky'}
|
|
@@ -8,6 +8,7 @@ import type CurvesEditorComponent from "./CurvesEditor.svelte";
|
|
|
8
8
|
import type ControlSliderComponent from "./ControlSlider.svelte";
|
|
9
9
|
import type SelectionOptionsBarComponent from "./SelectionOptionsBar.svelte";
|
|
10
10
|
import type RefineEdgePanelComponent from "./RefineEdgePanel.svelte";
|
|
11
|
+
import type SelectAndMaskWorkspaceComponent from "./SelectAndMaskWorkspace.svelte";
|
|
11
12
|
import type ChannelsPanelComponent from "./ChannelsPanel.svelte";
|
|
12
13
|
import type LayersPanelComponent from "./LayersPanel.svelte";
|
|
13
14
|
import type ColorProfilePanelComponent from "./ColorProfilePanel.svelte";
|
|
@@ -32,6 +33,7 @@ export { default as CurvesEditor } from './CurvesEditor.svelte';
|
|
|
32
33
|
export { default as ControlSlider } from './ControlSlider.svelte';
|
|
33
34
|
export { default as SelectionOptionsBar } from './SelectionOptionsBar.svelte';
|
|
34
35
|
export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
|
|
36
|
+
export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
|
|
35
37
|
export { default as ChannelsPanel } from './ChannelsPanel.svelte';
|
|
36
38
|
export { default as LayersPanel } from './LayersPanel.svelte';
|
|
37
39
|
export { default as ColorProfilePanel } from './ColorProfilePanel.svelte';
|
|
@@ -58,6 +60,7 @@ export type CurvesEditorProps = ComponentProps<typeof CurvesEditorComponent>;
|
|
|
58
60
|
export type ControlSliderProps = ComponentProps<typeof ControlSliderComponent>;
|
|
59
61
|
export type SelectionOptionsBarProps = ComponentProps<typeof SelectionOptionsBarComponent>;
|
|
60
62
|
export type RefineEdgePanelProps = ComponentProps<typeof RefineEdgePanelComponent>;
|
|
63
|
+
export type SelectAndMaskWorkspaceProps = ComponentProps<typeof SelectAndMaskWorkspaceComponent>;
|
|
61
64
|
export type ChannelsPanelProps = ComponentProps<typeof ChannelsPanelComponent>;
|
|
62
65
|
export type LayersPanelProps = ComponentProps<typeof LayersPanelComponent>;
|
|
63
66
|
export type ColorProfilePanelProps = ComponentProps<typeof ColorProfilePanelComponent>;
|
|
@@ -10,6 +10,7 @@ export { default as CurvesEditor } from './CurvesEditor.svelte';
|
|
|
10
10
|
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
|
+
export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
|
|
13
14
|
export { default as ChannelsPanel } from './ChannelsPanel.svelte';
|
|
14
15
|
export { default as LayersPanel } from './LayersPanel.svelte';
|
|
15
16
|
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.152.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.208.0"
|
|
169
169
|
},
|
|
170
170
|
"peerDependencies": {
|
|
171
171
|
"@dagrejs/dagre": "^1.1.4",
|