@pixerate/editor-svelte 0.162.2 → 0.164.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.
@@ -24,6 +24,7 @@
24
24
  import { untrack } from 'svelte';
25
25
  import LayerEffectsPanel from './LayerEffectsPanel.svelte';
26
26
  import EdgeColorPanel from './EdgeColorPanel.svelte';
27
+ import VectorMaskEdgesPanel from './VectorMaskEdgesPanel.svelte';
27
28
 
28
29
  interface Props {
29
30
  controller: ImageEditorController;
@@ -289,6 +290,9 @@
289
290
  <EdgeColorPanel {controller} layer={snapshot.active} class="border-t border-border pt-1.5" />
290
291
  {/key}
291
292
  {/if}
293
+ {#if snapshot.active?.vectorMask}
294
+ <VectorMaskEdgesPanel {controller} layer={snapshot.active} class="border-t border-border pt-1.5" />
295
+ {/if}
292
296
 
293
297
  {#if effects && snapshot.active && snapshot.active.type !== 'adjustment'}
294
298
  <LayerEffectsPanel {controller} class="border-t border-border pt-1.5" />
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import SelectionRecipePanel from './SelectionRecipePanel.svelte';
2
3
  import ImageCheckbox from './controls/ImageCheckbox.svelte';
3
4
  import ImageRange from './controls/ImageRange.svelte';
4
5
  import ImageSelect from './controls/ImageSelect.svelte';
@@ -590,4 +591,5 @@
590
591
  <p role="status" class="text-muted-foreground">{colorStatus}</p>
591
592
  {/if}
592
593
  <p data-testid="selection-status" class="text-muted-foreground">{describeSelection(sel)}</p>
594
+ <SelectionRecipePanel {controller} selection={sel} />
593
595
  </div>
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ import type { ImageEditorController, SelectionMask, SelectionRecipeStep } from '@pixerate/editor/image-editor';
3
+ import { describeRecipeStep, recipeStepSetting } from '@pixerate/editor/image-editor';
4
+ import { untrack } from 'svelte';
5
+
6
+ /**
7
+ * Live selection: the steps that made the selection, editable afterwards (each change replays
8
+ * them as one undo step), as the `*_selection_step` tools do. Freeze keeps the selection and drops
9
+ * the steps. Hidden while the selection is frozen or empty.
10
+ */
11
+ interface Props {
12
+ controller: ImageEditorController;
13
+ /** The current selection (`state.selection`), so the panel re-renders with it. */
14
+ selection?: SelectionMask | null;
15
+ class?: string;
16
+ }
17
+
18
+ let { controller, selection, class: className = '' }: Props = $props();
19
+
20
+ const MODES = ['replace', 'add', 'subtract', 'intersect'] as const;
21
+ const button =
22
+ 'px-1.5 py-0.5 rounded border border-border bg-background text-[11px] text-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40';
23
+ let tick = $state(0);
24
+ $effect(() => {
25
+ const c = controller;
26
+ return untrack(() => c.subscribe(() => tick++));
27
+ });
28
+ let error = $state<string | null>(null);
29
+ const recipe = $derived.by(() => {
30
+ void tick;
31
+ void selection;
32
+ return controller.getSelectionRecipe();
33
+ });
34
+ const isShape = (step: { op: string }) => ['rect', 'ellipse', 'polygon', 'magic-wand', 'color-range', 'color-range-preset'].includes(step.op);
35
+
36
+ function run(p: Promise<unknown> | unknown) {
37
+ error = null;
38
+ Promise.resolve(p).catch((err) => (error = err instanceof Error ? err.message : String(err)));
39
+ }
40
+ </script>
41
+
42
+ {#if recipe.live}
43
+ <div data-testid="selection-recipe" class="flex flex-col gap-1 text-xs {className}">
44
+ <div class="flex items-center justify-between">
45
+ <span class="text-muted-foreground">Selection steps</span>
46
+ <button type="button" class={button} title="Keep the selection, drop its steps" onclick={() => run(controller.freezeSelection())}>Freeze</button>
47
+ </div>
48
+ <ol class="flex flex-col gap-1">
49
+ {#each recipe.steps as step (`${step.index}:${JSON.stringify(step)}`)}
50
+ {@const setting = recipeStepSetting(step)}
51
+ <li data-testid="selection-step" class="flex flex-wrap items-center gap-1">
52
+ <span class="flex-1 min-w-0 truncate text-foreground">{describeRecipeStep(step as SelectionRecipeStep)}</span>
53
+ {#if isShape(step) && step.index > 0}
54
+ <select
55
+ aria-label="Step {step.index + 1} mode"
56
+ value={(step as { mode?: string }).mode ?? 'replace'}
57
+ onchange={(e) => run(controller.editSelectionStep(step.index, { mode: (e.currentTarget as HTMLSelectElement).value }))}
58
+ class="bg-background border border-border rounded px-1 py-0.5 text-foreground"
59
+ >
60
+ {#each MODES as m (m)}
61
+ <option value={m}>{m}</option>
62
+ {/each}
63
+ </select>
64
+ {/if}
65
+ {#if setting}
66
+ <label class="flex items-center gap-1 text-muted-foreground">
67
+ {setting.label}
68
+ <input
69
+ type="number"
70
+ min={setting.min}
71
+ max={setting.max}
72
+ aria-label="Step {step.index + 1} {setting.label}"
73
+ value={setting.value}
74
+ onblur={(e) => {
75
+ const v = Number((e.currentTarget as HTMLInputElement).value);
76
+ if (v !== setting.value) run(controller.editSelectionStep(step.index, setting.change(v)));
77
+ }}
78
+ onkeydown={(e) => e.key === 'Enter' && (e.currentTarget as HTMLInputElement).blur()}
79
+ class="w-12 bg-background border border-border rounded px-1 py-0.5 text-foreground"
80
+ />
81
+ </label>
82
+ {/if}
83
+ <button type="button" class={button} aria-label="Remove step {step.index + 1}" disabled={recipe.steps.length === 1} onclick={() => run(controller.removeSelectionStep(step.index))}>×</button>
84
+ </li>
85
+ {/each}
86
+ </ol>
87
+ {#if error}
88
+ <p role="alert" class="text-destructive">{error}</p>
89
+ {/if}
90
+ </div>
91
+ {/if}
@@ -0,0 +1,15 @@
1
+ import type { ImageEditorController, SelectionMask } from '@pixerate/editor/image-editor';
2
+ /**
3
+ * Live selection: the steps that made the selection, editable afterwards (each change replays
4
+ * them as one undo step), as the `*_selection_step` tools do. Freeze keeps the selection and drops
5
+ * the steps. Hidden while the selection is frozen or empty.
6
+ */
7
+ interface Props {
8
+ controller: ImageEditorController;
9
+ /** The current selection (`state.selection`), so the panel re-renders with it. */
10
+ selection?: SelectionMask | null;
11
+ class?: string;
12
+ }
13
+ declare const SelectionRecipePanel: import("svelte").Component<Props, {}, "">;
14
+ type SelectionRecipePanel = ReturnType<typeof SelectionRecipePanel>;
15
+ export default SelectionRecipePanel;
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import type { ImageEditorController, ImageLayer, VectorMaskEdge } from '@pixerate/editor/image-editor';
3
+ import { untrack } from 'svelte';
4
+
5
+ /**
6
+ * Hybrid mask edges: the soft segments of a layer's vector mask (feather or matte), with their
7
+ * settings, as `set_vector_mask_edge` sets them. Alt+Shift-click a segment with Direct Selection
8
+ * to cycle its mode on the canvas. Every change is one undo step.
9
+ */
10
+ interface Props {
11
+ controller: ImageEditorController;
12
+ /** The layer whose vector mask to edit. */
13
+ layer: ImageLayer;
14
+ class?: string;
15
+ }
16
+
17
+ let { controller, layer, class: className = '' }: Props = $props();
18
+
19
+ const MODES = ['hard', 'feather', 'matte'] as const;
20
+ const input = 'w-12 bg-background border border-border rounded px-1 py-0.5 text-foreground';
21
+ let tick = $state(0);
22
+ $effect(() => {
23
+ const c = controller;
24
+ return untrack(() => c.subscribe(() => tick++));
25
+ });
26
+ let error = $state<string | null>(null);
27
+ const info = $derived.by(() => {
28
+ void tick;
29
+ return controller.getVectorMask(layer.id);
30
+ });
31
+
32
+ function set(e: VectorMaskEdge, mode: VectorMaskEdge['mode'], options: { radius?: number; inner?: number; outer?: number } = {}) {
33
+ error = null;
34
+ try {
35
+ controller.setVectorMaskEdge(layer.id, { component: e.component, subpath: e.subpath, segment: e.segment }, mode, options);
36
+ } catch (err) {
37
+ error = err instanceof Error ? err.message : String(err);
38
+ }
39
+ }
40
+ const num = (v: string, fallback: number) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : fallback);
41
+ const value = (ev: Event) => (ev.currentTarget as HTMLInputElement | HTMLSelectElement).value;
42
+ </script>
43
+
44
+ {#if info}
45
+ <div class="flex flex-col gap-1.5 text-xs {className}" data-testid="vector-mask-edges">
46
+ <span class="text-muted-foreground">Vector mask edges</span>
47
+ <p class="text-muted-foreground">Alt+Shift-click a segment with Direct Selection to make it feathered or a hair matte.</p>
48
+ {#if info.edges.length === 0}
49
+ <p class="text-muted-foreground">Every segment is hard.</p>
50
+ {/if}
51
+ <ul class="flex flex-col gap-1">
52
+ {#each info.edges as e (`${e.component}:${e.subpath}:${e.segment}`)}
53
+ <li data-testid="vector-mask-edge" class="flex flex-wrap items-center gap-1.5">
54
+ <span class="font-mono text-muted-foreground">{e.component}/{e.subpath}/{e.segment}</span>
55
+ <select aria-label="Edge mode" value={e.mode} onchange={(ev) => set(e, value(ev) as VectorMaskEdge['mode'])} class="bg-background border border-border rounded px-1 py-0.5 text-foreground">
56
+ {#each MODES as m (m)}
57
+ <option value={m}>{m}</option>
58
+ {/each}
59
+ </select>
60
+ {#if e.mode === 'feather'}
61
+ <label class="flex items-center gap-1 text-muted-foreground">
62
+ radius
63
+ <input type="number" min="0.5" max="250" step="0.5" aria-label="Edge feather radius" value={e.radius} oninput={(ev) => set(e, 'feather', { radius: num(value(ev), 4) })} class={input} />
64
+ </label>
65
+ {/if}
66
+ {#if e.mode === 'matte'}
67
+ <label class="flex items-center gap-1 text-muted-foreground">
68
+ in
69
+ <input type="number" min="0.5" max="200" step="0.5" aria-label="Edge matte inner" value={e.inner} oninput={(ev) => set(e, 'matte', { inner: num(value(ev), 4), outer: e.outer })} class={input} />
70
+ </label>
71
+ <label class="flex items-center gap-1 text-muted-foreground">
72
+ out
73
+ <input type="number" min="0.5" max="200" step="0.5" aria-label="Edge matte outer" value={e.outer} oninput={(ev) => set(e, 'matte', { inner: e.inner, outer: num(value(ev), 8) })} class={input} />
74
+ </label>
75
+ {/if}
76
+ </li>
77
+ {/each}
78
+ </ul>
79
+ {#if error}
80
+ <p role="alert" class="text-destructive">{error}</p>
81
+ {/if}
82
+ </div>
83
+ {/if}
@@ -0,0 +1,15 @@
1
+ import type { ImageEditorController, ImageLayer } from '@pixerate/editor/image-editor';
2
+ /**
3
+ * Hybrid mask edges: the soft segments of a layer's vector mask (feather or matte), with their
4
+ * settings, as `set_vector_mask_edge` sets them. Alt+Shift-click a segment with Direct Selection
5
+ * to cycle its mode on the canvas. Every change is one undo step.
6
+ */
7
+ interface Props {
8
+ controller: ImageEditorController;
9
+ /** The layer whose vector mask to edit. */
10
+ layer: ImageLayer;
11
+ class?: string;
12
+ }
13
+ declare const VectorMaskEdgesPanel: import("svelte").Component<Props, {}, "">;
14
+ type VectorMaskEdgesPanel = ReturnType<typeof VectorMaskEdgesPanel>;
15
+ export default VectorMaskEdgesPanel;
@@ -11,6 +11,7 @@ import type RefineEdgePanelComponent from "./RefineEdgePanel.svelte";
11
11
  import type MaskEdgeCheckComponent from "./MaskEdgeCheck.svelte";
12
12
  import type SelectionChangeCheckComponent from "./SelectionChangeCheck.svelte";
13
13
  import type SeparateSubjectPanelComponent from "./SeparateSubjectPanel.svelte";
14
+ import type SelectionRecipePanelComponent from "./SelectionRecipePanel.svelte";
14
15
  import type PathsPanelComponent from "./PathsPanel.svelte";
15
16
  import type SelectAndMaskWorkspaceComponent from "./SelectAndMaskWorkspace.svelte";
16
17
  import type ContentAwareFillWorkspaceComponent from "./ContentAwareFillWorkspace.svelte";
@@ -19,6 +20,7 @@ import type LayersPanelComponent from "./LayersPanel.svelte";
19
20
  import type ColorProfilePanelComponent from "./ColorProfilePanel.svelte";
20
21
  import type LayerEffectsPanelComponent from "./LayerEffectsPanel.svelte";
21
22
  import type EdgeColorPanelComponent from "./EdgeColorPanel.svelte";
23
+ import type VectorMaskEdgesPanelComponent from "./VectorMaskEdgesPanel.svelte";
22
24
  import type BrushOptionsBarComponent from "./BrushOptionsBar.svelte";
23
25
  import type FreeTransformOptionsBarComponent from "./FreeTransformOptionsBar.svelte";
24
26
  import type ShapeOptionsBarComponent from "./ShapeOptionsBar.svelte";
@@ -42,6 +44,7 @@ export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
42
44
  export { default as MaskEdgeCheck } from './MaskEdgeCheck.svelte';
43
45
  export { default as SelectionChangeCheck } from './SelectionChangeCheck.svelte';
44
46
  export { default as SeparateSubjectPanel } from './SeparateSubjectPanel.svelte';
47
+ export { default as SelectionRecipePanel } from './SelectionRecipePanel.svelte';
45
48
  export { default as PathsPanel } from './PathsPanel.svelte';
46
49
  export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
47
50
  export { default as ContentAwareFillWorkspace } from './ContentAwareFillWorkspace.svelte';
@@ -50,6 +53,7 @@ export { default as LayersPanel } from './LayersPanel.svelte';
50
53
  export { default as ColorProfilePanel } from './ColorProfilePanel.svelte';
51
54
  export { default as LayerEffectsPanel } from './LayerEffectsPanel.svelte';
52
55
  export { default as EdgeColorPanel } from './EdgeColorPanel.svelte';
56
+ export { default as VectorMaskEdgesPanel } from './VectorMaskEdgesPanel.svelte';
53
57
  export { default as BrushOptionsBar } from './BrushOptionsBar.svelte';
54
58
  export { default as FreeTransformOptionsBar } from './FreeTransformOptionsBar.svelte';
55
59
  export { default as ShapeOptionsBar } from './ShapeOptionsBar.svelte';
@@ -75,6 +79,7 @@ export type RefineEdgePanelProps = ComponentProps<typeof RefineEdgePanelComponen
75
79
  export type MaskEdgeCheckProps = ComponentProps<typeof MaskEdgeCheckComponent>;
76
80
  export type SelectionChangeCheckProps = ComponentProps<typeof SelectionChangeCheckComponent>;
77
81
  export type SeparateSubjectPanelProps = ComponentProps<typeof SeparateSubjectPanelComponent>;
82
+ export type SelectionRecipePanelProps = ComponentProps<typeof SelectionRecipePanelComponent>;
78
83
  export type PathsPanelProps = ComponentProps<typeof PathsPanelComponent>;
79
84
  export type SelectAndMaskWorkspaceProps = ComponentProps<typeof SelectAndMaskWorkspaceComponent>;
80
85
  export type ContentAwareFillWorkspaceProps = ComponentProps<typeof ContentAwareFillWorkspaceComponent>;
@@ -83,6 +88,7 @@ export type LayersPanelProps = ComponentProps<typeof LayersPanelComponent>;
83
88
  export type ColorProfilePanelProps = ComponentProps<typeof ColorProfilePanelComponent>;
84
89
  export type LayerEffectsPanelProps = ComponentProps<typeof LayerEffectsPanelComponent>;
85
90
  export type EdgeColorPanelProps = ComponentProps<typeof EdgeColorPanelComponent>;
91
+ export type VectorMaskEdgesPanelProps = ComponentProps<typeof VectorMaskEdgesPanelComponent>;
86
92
  export type BrushOptionsBarProps = ComponentProps<typeof BrushOptionsBarComponent>;
87
93
  export type FreeTransformOptionsBarProps = ComponentProps<typeof FreeTransformOptionsBarComponent>;
88
94
  export type ShapeOptionsBarProps = ComponentProps<typeof ShapeOptionsBarComponent>;
@@ -13,6 +13,7 @@ export { default as RefineEdgePanel } from './RefineEdgePanel.svelte';
13
13
  export { default as MaskEdgeCheck } from './MaskEdgeCheck.svelte';
14
14
  export { default as SelectionChangeCheck } from './SelectionChangeCheck.svelte';
15
15
  export { default as SeparateSubjectPanel } from './SeparateSubjectPanel.svelte';
16
+ export { default as SelectionRecipePanel } from './SelectionRecipePanel.svelte';
16
17
  export { default as PathsPanel } from './PathsPanel.svelte';
17
18
  export { default as SelectAndMaskWorkspace } from './SelectAndMaskWorkspace.svelte';
18
19
  export { default as ContentAwareFillWorkspace } from './ContentAwareFillWorkspace.svelte';
@@ -21,6 +22,7 @@ export { default as LayersPanel } from './LayersPanel.svelte';
21
22
  export { default as ColorProfilePanel } from './ColorProfilePanel.svelte';
22
23
  export { default as LayerEffectsPanel } from './LayerEffectsPanel.svelte';
23
24
  export { default as EdgeColorPanel } from './EdgeColorPanel.svelte';
25
+ export { default as VectorMaskEdgesPanel } from './VectorMaskEdgesPanel.svelte';
24
26
  export { default as BrushOptionsBar } from './BrushOptionsBar.svelte';
25
27
  export { default as FreeTransformOptionsBar } from './FreeTransformOptionsBar.svelte';
26
28
  export { default as ShapeOptionsBar } from './ShapeOptionsBar.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.162.2",
3
+ "version": "0.164.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.219.2"
168
+ "@pixerate/editor": "0.221.0"
169
169
  },
170
170
  "peerDependencies": {
171
171
  "@dagrejs/dagre": "^1.1.4",