@pixerate/editor-svelte 0.155.0 → 0.157.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.
@@ -16,7 +16,7 @@
16
16
  interface Props {
17
17
  controller: ImageEditorController;
18
18
  /** Called with the result after OK, or `null` after Cancel. */
19
- onclose: (result: { layerId: string; bounds: CropBox; output: 'current' | 'new-layer' } | null) => void;
19
+ onclose: (result: { layerId: string; bounds: CropBox; output: 'current' | 'new-layer' | 'duplicate-layer' } | null) => void;
20
20
  class?: string;
21
21
  }
22
22
 
@@ -53,6 +53,10 @@
53
53
  void tick;
54
54
  return session.computing;
55
55
  });
56
+ const wholeImage = $derived.by(() => {
57
+ void tick;
58
+ return session.showsWholeImage;
59
+ });
56
60
  const sessionError = $derived.by(() => {
57
61
  void tick;
58
62
  return session.error;
@@ -164,6 +168,9 @@
164
168
  </div>
165
169
  {/if}
166
170
  <span class="font-mono text-muted-foreground">{Math.round(zoom * 100)}%</span>
171
+ {#if !wholeImage}
172
+ <button type="button" class={button} title="Show (and sample from) more of the image around the selection" onclick={() => session.expandView()}>Expand View</button>
173
+ {/if}
167
174
  {#if computing}
168
175
  <span role="status" class="text-muted-foreground">Updating preview…</span>
169
176
  {/if}
@@ -250,8 +257,9 @@
250
257
  options={[
251
258
  { value: 'new-layer', label: 'New Layer' },
252
259
  { value: 'current', label: 'Current Layer' },
260
+ { value: 'duplicate-layer', label: 'Duplicate Layer' },
253
261
  ]}
254
- onchange={(v) => set({ output: v as 'current' | 'new-layer' })}
262
+ onchange={(v) => set({ output: v as 'current' | 'new-layer' | 'duplicate-layer' })}
255
263
  />
256
264
  </label>
257
265
  {#if error ?? sessionError}
@@ -11,7 +11,7 @@ interface Props {
11
11
  onclose: (result: {
12
12
  layerId: string;
13
13
  bounds: CropBox;
14
- output: 'current' | 'new-layer';
14
+ output: 'current' | 'new-layer' | 'duplicate-layer';
15
15
  } | null) => void;
16
16
  class?: string;
17
17
  }
@@ -19,6 +19,7 @@
19
19
  FreeTransformTool,
20
20
  DEFAULT_SHAPE_TOOL_OPTIONS,
21
21
  DEFAULT_TYPE_TOOL_OPTIONS,
22
+ imageToolLabel,
22
23
  type SelectionToolOptions,
23
24
  type ShapeToolOptions,
24
25
  type TextEditSession,
@@ -702,16 +703,16 @@
702
703
 
703
704
  {#if activeTab === 'annotate'}
704
705
  <div class="flex flex-col gap-3 text-xs">
705
- <span class={imageSectionTitleClass}>Shape & Type Layers</span>
706
+ <span class={imageSectionTitleClass}>Shape & Type Layers, Paths</span>
706
707
  <div class="grid grid-cols-2 gap-1.5">
707
- {#each ['shape', 'type'] as const as t (t)}
708
+ {#each ['shape', 'type', 'path-pen', 'path-select', 'direct-select'] as const as t (t)}
708
709
  <button
709
710
  type="button"
710
711
  aria-pressed={editor.state.activeTool === t}
711
712
  onclick={() => editor.setTool(t)}
712
713
  class="py-1.5 px-2 rounded-lg font-medium border transition-colors {editor.state.activeTool === t
713
714
  ? 'bg-primary border-primary text-primary-foreground'
714
- : 'bg-muted border-border text-foreground hover:bg-accent'}">{t === 'shape' ? 'Shape' : 'Type'}</button>
715
+ : 'bg-muted border-border text-foreground hover:bg-accent'}">{imageToolLabel(t)}</button>
715
716
  {/each}
716
717
  </div>
717
718
  {#if editor.state.activeTool === 'shape'}
@@ -742,7 +743,7 @@
742
743
  ? 'bg-primary border-primary text-primary-foreground'
743
744
  : 'bg-muted border-border text-foreground hover:bg-accent'}"
744
745
  >
745
- {t}
746
+ {t === 'pen' ? 'freehand' : t}
746
747
  </button>
747
748
  {/each}
748
749
  </div>
@@ -27,6 +27,8 @@
27
27
  handleQuickMaskKey,
28
28
  handleMaskViewKey,
29
29
  ShapeDrawGesture,
30
+ PathEditGesture,
31
+ isPathEditTool,
30
32
  TypeToolGesture,
31
33
  DEFAULT_SHAPE_TOOL_OPTIONS,
32
34
  DEFAULT_TYPE_TOOL_OPTIONS,
@@ -279,6 +281,8 @@
279
281
 
280
282
  // Shape and type tools; type is edited on the canvas through a hidden textarea.
281
283
  const shapeGesture = $derived(new ShapeDrawGesture(editor.controller, () => shapeOptions));
284
+ // Path tools (Path Selection, Direct Selection, Pen): edit the active path, shape or vector mask.
285
+ const pathGesture = $derived(new PathEditGesture(editor.controller));
282
286
  const typeTool = $derived(new TypeToolGesture(editor.controller, () => typeOptions));
283
287
  let typeInputEl = $state<HTMLTextAreaElement | null>(null);
284
288
  $effect(() => {
@@ -521,7 +525,10 @@
521
525
  const viewport = measureCanvasViewport(containerEl);
522
526
  view = { width: viewport.width, height: viewport.height };
523
527
  // The transform overlay's rotate handle sits a fixed distance out on screen.
524
- transformTool.setView(getViewportProjection(editor.controller.getState(), view.width, view.height, mode === 'crop').scale);
528
+ const viewScale = getViewportProjection(editor.controller.getState(), view.width, view.height, mode === 'crop').scale;
529
+ transformTool.setView(viewScale);
530
+ pathGesture.setView(viewScale);
531
+ const activeTool = editor.controller.getState().activeTool;
525
532
 
526
533
  await editor.controller.render(canvasEl, {
527
534
  viewportWidth: viewport.width,
@@ -542,6 +549,7 @@
542
549
  : null,
543
550
  freeTransform: transformTool.overlay() ?? layerFrame(),
544
551
  textEdit: typeTool.session?.alive ? typeTool.session.overlay() : null,
552
+ pathEdit: mode === 'annotate' && isPathEditTool(activeTool) ? pathGesture.overlay(activeTool) : null,
545
553
  });
546
554
  }
547
555
 
@@ -623,6 +631,24 @@
623
631
  return () => window.removeEventListener('keydown', onKey);
624
632
  });
625
633
 
634
+ $effect(() => {
635
+ if (mode !== 'annotate' || typeof window === 'undefined') return;
636
+ const gesture = pathGesture;
637
+ // The path tools: Delete removes the selected anchors, Enter / Escape end the Pen's subpath.
638
+ const onKey = (e: KeyboardEvent) => {
639
+ const target = e.target as HTMLElement | null;
640
+ if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
641
+ if (!isPathEditTool(editor.controller.getState().activeTool)) return;
642
+ if (gesture.key(e.key)) {
643
+ e.preventDefault();
644
+ report('path', gesture.error);
645
+ void renderCanvas();
646
+ }
647
+ };
648
+ window.addEventListener('keydown', onKey);
649
+ return () => window.removeEventListener('keydown', onKey);
650
+ });
651
+
626
652
  $effect(() => {
627
653
  if (mode !== 'paint' || typeof window === 'undefined') return;
628
654
  const c = editor.controller;
@@ -728,6 +754,13 @@
728
754
  return;
729
755
  }
730
756
 
757
+ if (mode === 'annotate' && isPathEditTool(editor.state.activeTool)) {
758
+ pathGesture.down(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false), editor.state.activeTool, { shift: e.shiftKey, alt: e.altKey });
759
+ report('path', pathGesture.error);
760
+ void renderCanvas();
761
+ return;
762
+ }
763
+
731
764
  if (mode === 'annotate' && (editor.state.activeTool === 'shape' || editor.state.activeTool === 'type')) {
732
765
  const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
733
766
  report(editor.state.activeTool, null);
@@ -854,6 +887,16 @@
854
887
  return;
855
888
  }
856
889
 
890
+ if (mode === 'annotate' && isPathEditTool(editor.state.activeTool)) {
891
+ const imgPt = canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false);
892
+ if (pathGesture.active) {
893
+ pathGesture.move(imgPt, { shift: e.shiftKey, alt: e.altKey });
894
+ if (pathGesture.error) report('path', pathGesture.error);
895
+ } else pathGesture.hover(imgPt);
896
+ void renderCanvas();
897
+ return;
898
+ }
899
+
857
900
  if (mode === 'annotate' && editor.state.activeTool === 'shape') {
858
901
  if (shapeGesture.active) {
859
902
  shapeGesture.move(canvasToImagePoint(canvasPt, editor.state, view.width, view.height, false), { shift: e.shiftKey, alt: e.altKey });
@@ -975,6 +1018,11 @@
975
1018
  return;
976
1019
  }
977
1020
 
1021
+ if (mode === 'annotate' && isPathEditTool(editor.state.activeTool)) {
1022
+ pathGesture.up();
1023
+ void renderCanvas();
1024
+ return;
1025
+ }
978
1026
  if (mode === 'annotate' && editor.state.activeTool === 'shape') {
979
1027
  shapeGesture.up();
980
1028
  return;
@@ -1080,7 +1128,7 @@
1080
1128
  }}
1081
1129
  data-testid="image-editor-canvas"
1082
1130
  style="width: 100%; height: 100%;{panning ? ` cursor: ${isPanning ? 'grabbing' : 'grab'};` : transformActive || mode === 'transform' ? ` cursor: ${transformCursor};` : ''}"
1083
- class="absolute inset-0 w-full h-full block {transformActive || mode === 'transform' ? 'touch-none' : mode === 'paint' ? 'touch-none cursor-crosshair' : mode === 'annotate' && editor.state.activeTool === 'type' ? 'cursor-text' : mode === 'annotate' && editor.state.activeTool === 'shape' ? 'cursor-crosshair' : ''}"
1131
+ class="absolute inset-0 w-full h-full block {transformActive || mode === 'transform' ? 'touch-none' : mode === 'paint' ? 'touch-none cursor-crosshair' : mode === 'annotate' && editor.state.activeTool === 'type' ? 'cursor-text' : mode === 'annotate' && (editor.state.activeTool === 'shape' || editor.state.activeTool === 'path-pen') ? 'cursor-crosshair' : mode === 'annotate' && isPathEditTool(editor.state.activeTool) ? 'cursor-default' : ''}"
1084
1132
  ></canvas>
1085
1133
  {#if typeSession}
1086
1134
  <textarea
@@ -9,7 +9,8 @@
9
9
  * thumbnails. Click selects a path, double-click renames it (renaming the Work Path saves it).
10
10
  * Load as Selection (Shift adds, Alt subtracts), Make Work Path from the selection, Fill and
11
11
  * Stroke on the active layer with a colour and width, Add Vector Mask to the active layer,
12
- * Duplicate and Delete. Every change is one undo step on the controller.
12
+ * Duplicate and Delete. Clicking empty space deselects the path. Every change is one undo step on
13
+ * the controller.
13
14
  */
14
15
  interface Props {
15
16
  controller: ImageEditorController;
@@ -62,12 +63,15 @@
62
63
  }`;
63
64
  </script>
64
65
 
65
- <div data-testid="paths-panel" class="flex flex-col gap-2 text-xs {className}">
66
+ <!-- Clicking the panel's empty space deselects the path (the path tools then edit the active layer). -->
67
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
68
+ <div data-testid="paths-panel" class="flex flex-col gap-2 text-xs {className}" onclick={(e) => e.target === e.currentTarget && attempt(() => controller.setActivePath(null))}>
66
69
  <span class={imageSectionTitleClass}>Paths</span>
67
70
  {#if paths.length === 0}
68
71
  <p class="text-muted-foreground">No paths. Make a work path from a selection.</p>
69
72
  {/if}
70
- <ul role="list" class="flex flex-col gap-0.5">
73
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
74
+ <ul role="list" class="flex flex-col gap-0.5 min-h-4" onclick={(e) => e.target === e.currentTarget && attempt(() => controller.setActivePath(null))}>
71
75
  {#each paths as p (p.id)}
72
76
  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
73
77
  <li
@@ -4,7 +4,8 @@ import type { ImageEditorController } from '@pixerate/editor/image-editor';
4
4
  * thumbnails. Click selects a path, double-click renames it (renaming the Work Path saves it).
5
5
  * Load as Selection (Shift adds, Alt subtracts), Make Work Path from the selection, Fill and
6
6
  * Stroke on the active layer with a colour and width, Add Vector Mask to the active layer,
7
- * Duplicate and Delete. Every change is one undo step on the controller.
7
+ * Duplicate and Delete. Clicking empty space deselects the path. Every change is one undo step on
8
+ * the controller.
8
9
  */
9
10
  interface Props {
10
11
  controller: ImageEditorController;
@@ -1,4 +1,4 @@
1
1
  /** What pointer gestures on `ImageEditorCanvas` do (and whether the crop overlay shows). */
2
2
  export type ImageEditorCanvasMode = 'crop' | 'select' | 'paint' | 'annotate' | 'transform' | 'none';
3
3
  /** Which gesture an `ImageEditorCanvas` `onerror` report comes from. */
4
- export type ImageEditorCanvasErrorSource = 'paint' | 'select' | 'shape' | 'type' | 'transform';
4
+ export type ImageEditorCanvasErrorSource = 'paint' | 'select' | 'shape' | 'type' | 'transform' | 'path';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.155.0",
3
+ "version": "0.157.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.211.0"
168
+ "@pixerate/editor": "0.213.0"
169
169
  },
170
170
  "peerDependencies": {
171
171
  "@dagrejs/dagre": "^1.1.4",