@gradio/imageeditor 0.11.11 → 0.12.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @gradio/imageeditor
2
2
 
3
+ ## 0.12.1
4
+
5
+ ### Fixes
6
+
7
+ - [#10306](https://github.com/gradio-app/gradio/pull/10306) [`9fc988e`](https://github.com/gradio-app/gradio/commit/9fc988ebaf4bea24b71308922bb1889f1d3f4f6e) - Fix bug where ImageEditor always sends empty layers list to the backend. Thanks @freddyaboulton!
8
+
9
+ ## 0.12.0
10
+
11
+ ### Features
12
+
13
+ - [#10192](https://github.com/gradio-app/gradio/pull/10192) [`4fc7fb7`](https://github.com/gradio-app/gradio/commit/4fc7fb777c42af537e4af612423fa44029657d41) - Ensure components can be remounted with their previous data. Thanks @pngwn!
14
+
15
+ ### Dependency updates
16
+
17
+ - @gradio/atoms@0.13.0
18
+ - @gradio/utils@0.10.0
19
+ - @gradio/upload@0.14.4
20
+ - @gradio/client@1.9.0
21
+ - @gradio/icons@0.9.0
22
+ - @gradio/statustracker@0.10.0
23
+ - @gradio/wasm@0.16.0
24
+ - @gradio/image@0.20.0
25
+
3
26
  ## 0.11.11
4
27
 
5
28
  ### Dependency updates
package/Index.svelte CHANGED
@@ -57,6 +57,7 @@
57
57
  };
58
58
  export let canvas_size: [number, number] | undefined;
59
59
  export let show_fullscreen_button = true;
60
+ export let full_history: any = null;
60
61
 
61
62
  export let gradio: Gradio<{
62
63
  change: never;
@@ -185,6 +186,7 @@
185
186
  />
186
187
 
187
188
  <InteractiveImageEditor
189
+ on:history={(e) => (full_history = e.detail)}
188
190
  bind:dragging
189
191
  {canvas_size}
190
192
  on:change={() => handle_history_change()}
@@ -221,6 +223,7 @@
221
223
  upload={(...args) => gradio.client.upload(...args)}
222
224
  stream_handler={(...args) => gradio.client.stream(...args)}
223
225
  {placeholder}
226
+ {full_history}
224
227
  ></InteractiveImageEditor>
225
228
  </Block>
226
229
  {/if}
package/dist/Index.svelte CHANGED
@@ -42,6 +42,7 @@ export let attached_events = [];
42
42
  export let server;
43
43
  export let canvas_size;
44
44
  export let show_fullscreen_button = true;
45
+ export let full_history = null;
45
46
  export let gradio;
46
47
  let editor_instance;
47
48
  let image_id = null;
@@ -142,6 +143,7 @@ $:
142
143
  />
143
144
 
144
145
  <InteractiveImageEditor
146
+ on:history={(e) => (full_history = e.detail)}
145
147
  bind:dragging
146
148
  {canvas_size}
147
149
  on:change={() => handle_history_change()}
@@ -178,6 +180,7 @@ $:
178
180
  upload={(...args) => gradio.client.upload(...args)}
179
181
  stream_handler={(...args) => gradio.client.stream(...args)}
180
182
  {placeholder}
183
+ {full_history}
181
184
  ></InteractiveImageEditor>
182
185
  </Block>
183
186
  {/if}
@@ -36,6 +36,7 @@ declare const __propDef: {
36
36
  };
37
37
  canvas_size: [number, number] | undefined;
38
38
  show_fullscreen_button?: boolean | undefined;
39
+ full_history?: any;
39
40
  gradio: Gradio<{
40
41
  change: never;
41
42
  error: string;
@@ -158,6 +159,9 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
158
159
  get show_fullscreen_button(): boolean | undefined;
159
160
  /**accessor*/
160
161
  set show_fullscreen_button(_: boolean | undefined);
162
+ get full_history(): any;
163
+ /**accessor*/
164
+ set full_history(_: any);
161
165
  get gradio(): Gradio<{
162
166
  change: never;
163
167
  error: string;
@@ -22,6 +22,7 @@ const dispatch = createEventDispatcher();
22
22
  export let crop_constraint = false;
23
23
  export let canvas_size;
24
24
  export let parent_height;
25
+ export let full_history = null;
25
26
  $:
26
27
  orig_canvas_size = canvas_size;
27
28
  const BASE_DIMENSIONS = canvas_size || [800, 600];
@@ -54,8 +55,16 @@ const position_spring = spring(
54
55
  const pixi = writable(null);
55
56
  const CommandManager = command_manager();
56
57
  const { can_redo, can_undo, current_history } = CommandManager;
58
+ function get_start_history(history2) {
59
+ if (history2.previous) {
60
+ return get_start_history(history2.previous);
61
+ }
62
+ return history2;
63
+ }
57
64
  $:
58
65
  $current_history.previous, dispatch("change");
66
+ $:
67
+ dispatch("history", get_start_history($current_history));
59
68
  $: {
60
69
  history = !!$current_history.previous || $active_tool !== "bg";
61
70
  }
@@ -225,6 +234,11 @@ onMount(() => {
225
234
  }
226
235
  }
227
236
  resize(...$dimensions);
237
+ tick().then(() => {
238
+ if (full_history) {
239
+ CommandManager.hydrate(full_history);
240
+ }
241
+ });
228
242
  return () => {
229
243
  $pixi?.destroy();
230
244
  resizer.disconnect();
@@ -2,7 +2,7 @@ import { SvelteComponent } from "svelte";
2
2
  import type { Writable } from "svelte/store";
3
3
  import type { Spring } from "svelte/motion";
4
4
  import { type PixiApp } from "./utils/pixi";
5
- import { type CommandManager } from "./utils/commands";
5
+ import { type CommandManager, type CommandNode } from "./utils/commands";
6
6
  export declare const EDITOR_KEY: unique symbol;
7
7
  export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
8
8
  import { type tool } from "./tools";
@@ -52,6 +52,7 @@ declare const __propDef: {
52
52
  crop_constraint?: boolean | undefined;
53
53
  canvas_size: [number, number] | undefined;
54
54
  parent_height: number;
55
+ full_history?: (CommandNode | null) | undefined;
55
56
  height?: number | undefined;
56
57
  get_blobs?: (() => Promise<ImageBlobs>) | undefined;
57
58
  handle_remove?: (() => void) | undefined;
@@ -61,6 +62,7 @@ declare const __propDef: {
61
62
  clear?: CustomEvent<undefined> | undefined;
62
63
  save: CustomEvent<void>;
63
64
  change: CustomEvent<void>;
65
+ history: CustomEvent<Writable<CommandNode>>;
64
66
  } & {
65
67
  [evt: string]: CustomEvent<any>;
66
68
  };
@@ -3,6 +3,7 @@
3
3
  <script>import { createEventDispatcher } from "svelte";
4
4
  import {} from "@gradio/utils";
5
5
  import { prepare_files } from "@gradio/client";
6
+ import {} from "./utils/commands";
6
7
  import ImageEditor from "./ImageEditor.svelte";
7
8
  import Layers from "./layers/Layers.svelte";
8
9
  import {} from "./tools/Brush.svelte";
@@ -36,6 +37,7 @@ export let stream_handler;
36
37
  export let dragging;
37
38
  export let placeholder = void 0;
38
39
  export let height = 450;
40
+ export let full_history = null;
39
41
  const dispatch = createEventDispatcher();
40
42
  let editor;
41
43
  function is_not_null(o) {
@@ -157,6 +159,7 @@ $:
157
159
  label={label || i18n("image.image")}
158
160
  />
159
161
  <ImageEditor
162
+ on:history
160
163
  {canvas_size}
161
164
  crop_size={Array.isArray(crop_size) ? crop_size : undefined}
162
165
  bind:this={editor}
@@ -170,6 +173,7 @@ $:
170
173
  bind:bg
171
174
  {sources}
172
175
  crop_constraint={!!crop_constraint}
176
+ {full_history}
173
177
  >
174
178
  <Tools {i18n}>
175
179
  <Layers layer_files={value?.layers || null} enable_layers={layers} />
@@ -11,6 +11,7 @@ export interface ImageBlobs {
11
11
  }
12
12
  import { type I18nFormatter } from "@gradio/utils";
13
13
  import { type FileData, type Client } from "@gradio/client";
14
+ import { type CommandNode } from "./utils/commands";
14
15
  import { type Brush as IBrush } from "./tools/Brush.svelte";
15
16
  import { type Eraser } from "./tools/Brush.svelte";
16
17
  declare const __propDef: {
@@ -36,10 +37,12 @@ declare const __propDef: {
36
37
  dragging: boolean;
37
38
  placeholder?: string | undefined;
38
39
  height?: number | undefined;
40
+ full_history?: (CommandNode | null) | undefined;
39
41
  get_data?: (() => Promise<ImageBlobs>) | undefined;
40
42
  image_id?: (null | string) | undefined;
41
43
  };
42
44
  events: {
45
+ history: CustomEvent<import("svelte/store").Writable<CommandNode>>;
43
46
  save: CustomEvent<void>;
44
47
  clear?: CustomEvent<undefined> | undefined;
45
48
  upload?: CustomEvent<undefined> | undefined;
@@ -6,9 +6,20 @@ import { Layers } from "@gradio/icons";
6
6
  let show_layers = false;
7
7
  export let layer_files = [];
8
8
  export let enable_layers = true;
9
- const { pixi, current_layer, dimensions, register_context } = getContext(EDITOR_KEY);
9
+ const {
10
+ pixi,
11
+ current_layer,
12
+ dimensions,
13
+ register_context,
14
+ command_manager,
15
+ current_history
16
+ } = getContext(EDITOR_KEY);
17
+ const { can_undo } = command_manager;
10
18
  const LayerManager = layer_manager();
11
- let layers = [];
19
+ const manager_current_layer = LayerManager.active_layer;
20
+ const layers = LayerManager.layers;
21
+ $:
22
+ current_layer.set($manager_current_layer);
12
23
  register_context("layers", {
13
24
  init_fn: () => {
14
25
  new_layer();
@@ -18,7 +29,7 @@ register_context("layers", {
18
29
  }
19
30
  });
20
31
  async function validate_layers() {
21
- let invalid = layers.some(
32
+ let invalid = $layers.some(
22
33
  (layer) => layer.composite.texture?.width != $dimensions[0] || layer.composite.texture?.height != $dimensions[1]
23
34
  );
24
35
  if (invalid) {
@@ -34,13 +45,16 @@ $:
34
45
  async function new_layer() {
35
46
  if (!$pixi)
36
47
  return;
37
- const [active_layer, all_layers] = LayerManager.add_layer(
48
+ const new_layer2 = LayerManager.add_layer(
38
49
  $pixi.layer_container,
39
50
  $pixi.renderer,
40
51
  ...$dimensions
41
52
  );
42
- $current_layer = active_layer;
43
- layers = all_layers;
53
+ if ($can_undo || $layers.length > 0) {
54
+ command_manager.execute(new_layer2);
55
+ } else {
56
+ new_layer2.execute();
57
+ }
44
58
  }
45
59
  $:
46
60
  render_layer_files(layer_files);
@@ -71,19 +85,19 @@ async function render_layer_files(_layer_files) {
71
85
  })
72
86
  );
73
87
  LayerManager.reset();
74
- let last_layer = null;
75
88
  for (const blob of blobs.filter(is_not_null)) {
76
- last_layer = await LayerManager.add_layer_from_blob(
89
+ const new_layer2 = await LayerManager.add_layer_from_blob(
77
90
  $pixi.layer_container,
78
91
  $pixi.renderer,
79
92
  blob,
80
93
  $pixi.view
81
94
  );
95
+ if ($can_undo && $layers.length === 0) {
96
+ command_manager.execute(new_layer2);
97
+ } else {
98
+ new_layer2.execute();
99
+ }
82
100
  }
83
- if (!last_layer)
84
- return;
85
- $current_layer = last_layer[0];
86
- layers = last_layer[1];
87
101
  }
88
102
  onMount(async () => {
89
103
  await tick();
@@ -102,13 +116,13 @@ onMount(async () => {
102
116
  <button
103
117
  aria-label="Show Layers"
104
118
  on:click={() => (show_layers = !show_layers)}
105
- ><span class="icon"><Layers /></span> Layer {layers.findIndex(
119
+ ><span class="icon"><Layers /></span> Layer {$layers.findIndex(
106
120
  (l) => l === $current_layer
107
121
  ) + 1}
108
122
  </button>
109
123
  {#if show_layers}
110
124
  <ul>
111
- {#each layers as layer, i (i)}
125
+ {#each $layers as layer, i (i)}
112
126
  <li>
113
127
  <button
114
128
  class:selected_layer={$current_layer === layer}
@@ -1,4 +1,6 @@
1
1
  import { Container, type IRenderer, RenderTexture, Sprite, Filter } from "pixi.js";
2
+ import { type Command } from "../utils/commands";
3
+ import { type Writable } from "svelte/store";
2
4
  /**
3
5
  * GLSL Shader that takes two textures and erases the second texture from the first.
4
6
  */
@@ -35,7 +37,7 @@ interface LayerManager {
35
37
  * @param width the width of the layer
36
38
  * @param height the height of the layer
37
39
  */
38
- add_layer(container: Container, renderer: IRenderer, width: number, height: number): [LayerScene, LayerScene[]];
40
+ add_layer(container: Container, renderer: IRenderer, width: number, height: number, sprite?: Sprite): Command;
39
41
  /**
40
42
  * Swaps the layer with the layer above or below it.
41
43
  * @param layer The index layer to swap.
@@ -58,7 +60,9 @@ interface LayerManager {
58
60
  * @returns The layers.
59
61
  */
60
62
  get_layers(): LayerScene[];
61
- add_layer_from_blob(container: Container, renderer: IRenderer, blob: Blob, view: HTMLCanvasElement): Promise<[LayerScene, LayerScene[]]>;
63
+ layers: Writable<LayerScene[]>;
64
+ active_layer: Writable<LayerScene | null>;
65
+ add_layer_from_blob(container: Container, renderer: IRenderer, blob: Blob, view: HTMLCanvasElement): Promise<Command>;
62
66
  }
63
67
  /**
64
68
  * Creates a layer manager.
@@ -1,4 +1,6 @@
1
1
  import { Container, RenderTexture, Texture, Sprite, Filter } from "pixi.js";
2
+ import {} from "../utils/commands";
3
+ import { get, writable } from "svelte/store";
2
4
  /**
3
5
  * GLSL Shader that takes two textures and erases the second texture from the first.
4
6
  */
@@ -39,36 +41,58 @@ export function layer_manager() {
39
41
  let current_layer = 0;
40
42
  let position = 0;
41
43
  return {
42
- add_layer: function (container, renderer, width, height) {
43
- const layer_container = new Container();
44
- position++;
45
- layer_container.zIndex = position;
46
- const composite_texture = RenderTexture.create({
47
- width,
48
- height
49
- });
50
- const composite = new Sprite(composite_texture);
51
- layer_container.addChild(composite);
52
- composite.zIndex = position;
53
- const layer_scene = {
54
- draw_texture: RenderTexture.create({
55
- width,
56
- height
57
- }),
58
- erase_texture: RenderTexture.create({
59
- width,
60
- height
61
- }),
62
- composite
44
+ add_layer: function (container, renderer, width, height, sprite) {
45
+ let layer_container;
46
+ let layer_number;
47
+ let that = this;
48
+ return {
49
+ execute: function () {
50
+ layer_container = new Container();
51
+ position++;
52
+ layer_container.zIndex = position;
53
+ const composite_texture = RenderTexture.create({
54
+ width,
55
+ height
56
+ });
57
+ const composite = new Sprite(composite_texture);
58
+ layer_container.addChild(composite);
59
+ composite.zIndex = position;
60
+ const layer_scene = {
61
+ draw_texture: RenderTexture.create({
62
+ width,
63
+ height
64
+ }),
65
+ erase_texture: RenderTexture.create({
66
+ width,
67
+ height
68
+ }),
69
+ composite
70
+ };
71
+ const erase_filter = new Filter(undefined, erase_shader, {
72
+ uEraserTexture: layer_scene.erase_texture,
73
+ uDrawingTexture: layer_scene.draw_texture
74
+ });
75
+ composite.filters = [erase_filter];
76
+ container.addChild(layer_container);
77
+ _layers.push(layer_scene);
78
+ that.layers.update((s) => [...s, layer_scene]);
79
+ that.active_layer.set(layer_scene);
80
+ layer_number = get(that.layers).length - 1;
81
+ if (sprite) {
82
+ renderer.render(sprite, {
83
+ renderTexture: layer_scene.draw_texture
84
+ });
85
+ }
86
+ },
87
+ undo: function () {
88
+ container.removeChild(layer_container);
89
+ _layers = get(that.layers);
90
+ _layers = _layers.filter((_, i) => i !== layer_number);
91
+ that.layers.set(_layers);
92
+ const new_layer = _layers[layer_number - 1] || _layers[0] || null;
93
+ that.active_layer.set(new_layer);
94
+ }
63
95
  };
64
- const erase_filter = new Filter(undefined, erase_shader, {
65
- uEraserTexture: layer_scene.erase_texture,
66
- uDrawingTexture: layer_scene.draw_texture
67
- });
68
- composite.filters = [erase_filter];
69
- container.addChild(layer_container);
70
- _layers.push(layer_scene);
71
- return [layer_scene, _layers];
72
96
  },
73
97
  swap_layers: function (layer, direction) {
74
98
  if (direction === "up") {
@@ -80,7 +104,8 @@ export function layer_manager() {
80
104
  return _layers[layer];
81
105
  },
82
106
  change_active_layer: function (layer) {
83
- current_layer = layer;
107
+ // current_layer = layer;
108
+ this.active_layer.set(get(this.layers)[layer]);
84
109
  return _layers[layer];
85
110
  },
86
111
  reset() {
@@ -92,6 +117,8 @@ export function layer_manager() {
92
117
  _layers = [];
93
118
  current_layer = 0;
94
119
  position = 0;
120
+ this.active_layer.set(null);
121
+ this.layers.set([]);
95
122
  },
96
123
  async add_layer_from_blob(container, renderer, blob, view) {
97
124
  const img = await createImageBitmap(blob);
@@ -101,15 +128,13 @@ export function layer_manager() {
101
128
  sprite.zIndex = 0;
102
129
  sprite.width = w;
103
130
  sprite.height = h;
104
- const [layer, layers] = this.add_layer(container, renderer, view.width, view.height);
105
- renderer.render(sprite, {
106
- renderTexture: layer.draw_texture
107
- });
108
- return [layer, layers];
131
+ return this.add_layer(container, renderer, view.width, view.height, sprite);
109
132
  },
110
133
  get_layers() {
111
134
  return _layers;
112
- }
135
+ },
136
+ layers: writable([]),
137
+ active_layer: writable(null)
113
138
  };
114
139
  }
115
140
  function resize_to_fit(inner_width, inner_height, outer_width, outer_height) {
@@ -13,8 +13,12 @@ export let default_color = void 0;
13
13
  export let colors = void 0;
14
14
  export let color_mode = void 0;
15
15
  export let mode;
16
- $:
17
- processed_colors = colors ? colors.map(process_color).filter((_, i) => i < 4) : [];
16
+ let processed_colors = [];
17
+ let old_colors = [];
18
+ if (colors && JSON.stringify(old_colors) !== JSON.stringify(colors)) {
19
+ processed_colors = colors.map(process_color).filter((_, i) => i < 4);
20
+ old_colors = processed_colors;
21
+ }
18
22
  let selected_color = process_color(default_color || "#000000");
19
23
  $:
20
24
  if (mode === "draw") {
@@ -65,11 +65,16 @@ export interface CommandManager {
65
65
  * Observable store that you can subscribe to for updates
66
66
  */
67
67
  readonly current_history: Writable<CommandNode>;
68
+ /**
69
+ * Hydrates the command manager with a full history
70
+ * @param full_history the full history to hydrate with
71
+ */
72
+ hydrate(full_history: CommandNode): void;
68
73
  }
69
74
  /**
70
75
  * Command node interface that is used to create the undo/redo history
71
76
  */
72
- interface CommandNode {
77
+ export interface CommandNode {
73
78
  /**
74
79
  * Command that the node holds
75
80
  */
@@ -93,4 +98,3 @@ interface CommandNode {
93
98
  * @returns a command manager
94
99
  */
95
100
  export declare function command_manager(): CommandManager;
96
- export {};
@@ -52,6 +52,14 @@ export function command_manager() {
52
52
  can_redo.set(!!history.next);
53
53
  current_history.set(history);
54
54
  },
55
+ hydrate: function (full_history) {
56
+ setTimeout(() => {
57
+ while (full_history.next) {
58
+ this.execute(full_history.next.command);
59
+ full_history = full_history.next;
60
+ }
61
+ }, 1000);
62
+ },
55
63
  can_undo,
56
64
  can_redo,
57
65
  current_history,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/imageeditor",
3
- "version": "0.11.11",
3
+ "version": "0.12.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,14 +10,14 @@
10
10
  "@types/tinycolor2": "^1.4.6",
11
11
  "pixi.js": "^7.3.2",
12
12
  "tinycolor2": "^1.6.0",
13
- "@gradio/atoms": "^0.12.0",
14
- "@gradio/client": "^1.8.0",
15
- "@gradio/icons": "^0.8.1",
16
- "@gradio/image": "^0.19.0",
17
- "@gradio/statustracker": "^0.9.7",
18
- "@gradio/utils": "^0.9.0",
19
- "@gradio/wasm": "^0.15.0",
20
- "@gradio/upload": "^0.14.3"
13
+ "@gradio/client": "^1.9.0",
14
+ "@gradio/atoms": "^0.13.0",
15
+ "@gradio/icons": "^0.9.0",
16
+ "@gradio/image": "^0.20.0",
17
+ "@gradio/upload": "^0.14.4",
18
+ "@gradio/statustracker": "^0.10.0",
19
+ "@gradio/wasm": "^0.16.0",
20
+ "@gradio/utils": "^0.10.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@gradio/preview": "^0.13.0"
@@ -2,7 +2,7 @@
2
2
  import type { Writable, Readable } from "svelte/store";
3
3
  import type { Spring } from "svelte/motion";
4
4
  import { type PixiApp } from "./utils/pixi";
5
- import { type CommandManager } from "./utils/commands";
5
+ import { type CommandManager, type CommandNode } from "./utils/commands";
6
6
 
7
7
  export const EDITOR_KEY = Symbol("editor");
8
8
  export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
@@ -71,10 +71,12 @@
71
71
  clear?: never;
72
72
  save: void;
73
73
  change: void;
74
+ history: CommandManager["current_history"];
74
75
  }>();
75
76
  export let crop_constraint = false;
76
77
  export let canvas_size: [number, number] | undefined;
77
78
  export let parent_height: number;
79
+ export let full_history: CommandNode | null = null;
78
80
 
79
81
  $: orig_canvas_size = canvas_size;
80
82
 
@@ -114,7 +116,15 @@
114
116
 
115
117
  const { can_redo, can_undo, current_history } = CommandManager;
116
118
 
119
+ function get_start_history(history: any): any {
120
+ if (history.previous) {
121
+ return get_start_history(history.previous);
122
+ }
123
+ return history;
124
+ }
125
+
117
126
  $: $current_history.previous, dispatch("change");
127
+ $: dispatch("history", get_start_history($current_history));
118
128
 
119
129
  $: {
120
130
  history = !!$current_history.previous || $active_tool !== "bg";
@@ -316,6 +326,12 @@
316
326
 
317
327
  resize(...$dimensions);
318
328
 
329
+ tick().then(() => {
330
+ if (full_history) {
331
+ CommandManager.hydrate(full_history);
332
+ }
333
+ });
334
+
319
335
  return () => {
320
336
  $pixi?.destroy();
321
337
  resizer.disconnect();
@@ -16,7 +16,7 @@
16
16
  import { createEventDispatcher } from "svelte";
17
17
  import { type I18nFormatter } from "@gradio/utils";
18
18
  import { prepare_files, type FileData, type Client } from "@gradio/client";
19
-
19
+ import { type CommandNode } from "./utils/commands";
20
20
  import ImageEditor from "./ImageEditor.svelte";
21
21
  import Layers from "./layers/Layers.svelte";
22
22
  import { type Brush as IBrush } from "./tools/Brush.svelte";
@@ -57,6 +57,7 @@
57
57
  export let dragging: boolean;
58
58
  export let placeholder: string | undefined = undefined;
59
59
  export let height = 450;
60
+ export let full_history: CommandNode | null = null;
60
61
 
61
62
  const dispatch = createEventDispatcher<{
62
63
  clear?: never;
@@ -215,6 +216,7 @@
215
216
  label={label || i18n("image.image")}
216
217
  />
217
218
  <ImageEditor
219
+ on:history
218
220
  {canvas_size}
219
221
  crop_size={Array.isArray(crop_size) ? crop_size : undefined}
220
222
  bind:this={editor}
@@ -228,6 +230,7 @@
228
230
  bind:bg
229
231
  {sources}
230
232
  crop_constraint={!!crop_constraint}
233
+ {full_history}
231
234
  >
232
235
  <Tools {i18n}>
233
236
  <Layers layer_files={value?.layers || null} enable_layers={layers} />
@@ -12,11 +12,22 @@
12
12
  export let layer_files: (FileData | null)[] | null = [];
13
13
  export let enable_layers = true;
14
14
 
15
- const { pixi, current_layer, dimensions, register_context } =
16
- getContext<EditorContext>(EDITOR_KEY);
15
+ const {
16
+ pixi,
17
+ current_layer,
18
+ dimensions,
19
+ register_context,
20
+ command_manager,
21
+ current_history
22
+ } = getContext<EditorContext>(EDITOR_KEY);
23
+
24
+ const { can_undo } = command_manager;
17
25
 
18
26
  const LayerManager = layer_manager();
19
- let layers: LayerScene[] = [];
27
+ const manager_current_layer = LayerManager.active_layer;
28
+ const layers = LayerManager.layers;
29
+
30
+ $: current_layer.set($manager_current_layer);
20
31
 
21
32
  register_context("layers", {
22
33
  init_fn: () => {
@@ -28,7 +39,7 @@
28
39
  });
29
40
 
30
41
  async function validate_layers(): Promise<void> {
31
- let invalid = layers.some(
42
+ let invalid = $layers.some(
32
43
  (layer) =>
33
44
  layer.composite.texture?.width != $dimensions[0] ||
34
45
  layer.composite.texture?.height != $dimensions[1]
@@ -44,14 +55,17 @@
44
55
  async function new_layer(): Promise<void> {
45
56
  if (!$pixi) return;
46
57
 
47
- const [active_layer, all_layers] = LayerManager.add_layer(
58
+ const new_layer = LayerManager.add_layer(
48
59
  $pixi.layer_container,
49
60
  $pixi.renderer,
50
61
  ...$dimensions
51
62
  );
52
63
 
53
- $current_layer = active_layer;
54
- layers = all_layers;
64
+ if ($can_undo || $layers.length > 0) {
65
+ command_manager.execute(new_layer);
66
+ } else {
67
+ new_layer.execute();
68
+ }
55
69
  }
56
70
 
57
71
  $: render_layer_files(layer_files);
@@ -88,20 +102,20 @@
88
102
 
89
103
  LayerManager.reset();
90
104
 
91
- let last_layer: [LayerScene, LayerScene[]] | null = null;
92
105
  for (const blob of blobs.filter(is_not_null)) {
93
- last_layer = await LayerManager.add_layer_from_blob(
106
+ const new_layer = await LayerManager.add_layer_from_blob(
94
107
  $pixi.layer_container,
95
108
  $pixi.renderer,
96
109
  blob,
97
110
  $pixi.view
98
111
  );
99
- }
100
-
101
- if (!last_layer) return;
102
112
 
103
- $current_layer = last_layer[0];
104
- layers = last_layer[1];
113
+ if ($can_undo && $layers.length === 0) {
114
+ command_manager.execute(new_layer);
115
+ } else {
116
+ new_layer.execute();
117
+ }
118
+ }
105
119
  }
106
120
 
107
121
  onMount(async () => {
@@ -121,13 +135,13 @@
121
135
  <button
122
136
  aria-label="Show Layers"
123
137
  on:click={() => (show_layers = !show_layers)}
124
- ><span class="icon"><Layers /></span> Layer {layers.findIndex(
138
+ ><span class="icon"><Layers /></span> Layer {$layers.findIndex(
125
139
  (l) => l === $current_layer
126
140
  ) + 1}
127
141
  </button>
128
142
  {#if show_layers}
129
143
  <ul>
130
- {#each layers as layer, i (i)}
144
+ {#each $layers as layer, i (i)}
131
145
  <li>
132
146
  <button
133
147
  class:selected_layer={$current_layer === layer}
@@ -8,6 +8,9 @@ import {
8
8
  Filter
9
9
  } from "pixi.js";
10
10
 
11
+ import { type Command } from "../utils/commands";
12
+ import { get, writable, type Writable } from "svelte/store";
13
+
11
14
  /**
12
15
  * GLSL Shader that takes two textures and erases the second texture from the first.
13
16
  */
@@ -65,8 +68,9 @@ interface LayerManager {
65
68
  container: Container,
66
69
  renderer: IRenderer,
67
70
  width: number,
68
- height: number
69
- ): [LayerScene, LayerScene[]];
71
+ height: number,
72
+ sprite?: Sprite
73
+ ): Command;
70
74
  /**
71
75
  * Swaps the layer with the layer above or below it.
72
76
  * @param layer The index layer to swap.
@@ -89,13 +93,15 @@ interface LayerManager {
89
93
  * @returns The layers.
90
94
  */
91
95
  get_layers(): LayerScene[];
96
+ layers: Writable<LayerScene[]>;
97
+ active_layer: Writable<LayerScene | null>;
92
98
 
93
99
  add_layer_from_blob(
94
100
  container: Container,
95
101
  renderer: IRenderer,
96
102
  blob: Blob,
97
103
  view: HTMLCanvasElement
98
- ): Promise<[LayerScene, LayerScene[]]>;
104
+ ): Promise<Command>;
99
105
  }
100
106
 
101
107
  /**
@@ -126,47 +132,72 @@ export function layer_manager(): LayerManager {
126
132
  container: Container,
127
133
  renderer: IRenderer,
128
134
  width: number,
129
- height: number
130
- ): [LayerScene, LayerScene[]] {
131
- const layer_container = new Container() as Container & DisplayObject;
132
- position++;
133
- layer_container.zIndex = position;
134
-
135
- const composite_texture = RenderTexture.create({
136
- width,
137
- height
138
- });
139
-
140
- const composite = new Sprite(composite_texture) as Sprite & DisplayObject;
141
-
142
- layer_container.addChild(composite);
143
-
144
- composite.zIndex = position;
145
-
146
- const layer_scene: LayerScene = {
147
- draw_texture: RenderTexture.create({
148
- width,
149
- height
150
- }),
151
- erase_texture: RenderTexture.create({
152
- width,
153
- height
154
- }),
155
- composite
135
+ height: number,
136
+ sprite?: Sprite
137
+ ): Command {
138
+ let layer_container: Container & DisplayObject;
139
+ let layer_number: number;
140
+ let that = this;
141
+ return {
142
+ execute: function () {
143
+ layer_container = new Container() as Container & DisplayObject;
144
+ position++;
145
+ layer_container.zIndex = position;
146
+
147
+ const composite_texture = RenderTexture.create({
148
+ width,
149
+ height
150
+ });
151
+
152
+ const composite = new Sprite(composite_texture) as Sprite &
153
+ DisplayObject;
154
+
155
+ layer_container.addChild(composite);
156
+
157
+ composite.zIndex = position;
158
+
159
+ const layer_scene: LayerScene = {
160
+ draw_texture: RenderTexture.create({
161
+ width,
162
+ height
163
+ }),
164
+ erase_texture: RenderTexture.create({
165
+ width,
166
+ height
167
+ }),
168
+ composite
169
+ };
170
+
171
+ const erase_filter = new Filter(undefined, erase_shader, {
172
+ uEraserTexture: layer_scene.erase_texture,
173
+ uDrawingTexture: layer_scene.draw_texture
174
+ });
175
+
176
+ composite.filters = [erase_filter];
177
+
178
+ container.addChild(layer_container);
179
+ _layers.push(layer_scene);
180
+ that.layers.update((s) => [...s, layer_scene]);
181
+ that.active_layer.set(layer_scene);
182
+
183
+ layer_number = get(that.layers).length - 1;
184
+
185
+ if (sprite) {
186
+ renderer.render(sprite, {
187
+ renderTexture: layer_scene.draw_texture
188
+ });
189
+ }
190
+ },
191
+ undo: function () {
192
+ container.removeChild(layer_container);
193
+
194
+ _layers = get(that.layers);
195
+ _layers = _layers.filter((_, i) => i !== layer_number);
196
+ that.layers.set(_layers);
197
+ const new_layer = _layers[layer_number - 1] || _layers[0] || null;
198
+ that.active_layer.set(new_layer);
199
+ }
156
200
  };
157
-
158
- const erase_filter = new Filter(undefined, erase_shader, {
159
- uEraserTexture: layer_scene.erase_texture,
160
- uDrawingTexture: layer_scene.draw_texture
161
- });
162
-
163
- composite.filters = [erase_filter];
164
-
165
- container.addChild(layer_container);
166
-
167
- _layers.push(layer_scene);
168
-
169
- return [layer_scene, _layers];
170
201
  },
171
202
 
172
203
  swap_layers: function (
@@ -182,7 +213,8 @@ export function layer_manager(): LayerManager {
182
213
  },
183
214
 
184
215
  change_active_layer: function (layer: number): LayerScene {
185
- current_layer = layer;
216
+ // current_layer = layer;
217
+ this.active_layer.set(get(this.layers)[layer]);
186
218
  return _layers[layer];
187
219
  },
188
220
  reset() {
@@ -194,13 +226,15 @@ export function layer_manager(): LayerManager {
194
226
  _layers = [];
195
227
  current_layer = 0;
196
228
  position = 0;
229
+ this.active_layer.set(null);
230
+ this.layers.set([]);
197
231
  },
198
232
  async add_layer_from_blob(
199
233
  container: Container,
200
234
  renderer: IRenderer,
201
235
  blob: Blob,
202
236
  view: HTMLCanvasElement
203
- ) {
237
+ ): Promise<Command> {
204
238
  const img = await createImageBitmap(blob);
205
239
  const bitmap_texture = Texture.from(img);
206
240
 
@@ -217,22 +251,19 @@ export function layer_manager(): LayerManager {
217
251
  sprite.width = w;
218
252
  sprite.height = h;
219
253
 
220
- const [layer, layers] = this.add_layer(
254
+ return this.add_layer(
221
255
  container,
222
256
  renderer,
223
257
  view.width,
224
- view.height
258
+ view.height,
259
+ sprite
225
260
  );
226
-
227
- renderer.render(sprite, {
228
- renderTexture: layer.draw_texture
229
- });
230
-
231
- return [layer, layers];
232
261
  },
233
262
  get_layers() {
234
263
  return _layers;
235
- }
264
+ },
265
+ layers: writable([]),
266
+ active_layer: writable(null)
236
267
  };
237
268
  }
238
269
 
@@ -43,9 +43,12 @@
43
43
  export let color_mode: Brush["color_mode"] | undefined = undefined;
44
44
  export let mode: "erase" | "draw";
45
45
 
46
- $: processed_colors = colors
47
- ? colors.map(process_color).filter((_, i) => i < 4)
48
- : [];
46
+ let processed_colors: string[] = [];
47
+ let old_colors: string[] = [];
48
+ if (colors && JSON.stringify(old_colors) !== JSON.stringify(colors)) {
49
+ processed_colors = colors.map(process_color).filter((_, i) => i < 4);
50
+ old_colors = processed_colors;
51
+ }
49
52
 
50
53
  let selected_color = process_color(default_color || "#000000");
51
54
 
@@ -67,12 +67,17 @@ export interface CommandManager {
67
67
  * Observable store that you can subscribe to for updates
68
68
  */
69
69
  readonly current_history: Writable<CommandNode>;
70
+ /**
71
+ * Hydrates the command manager with a full history
72
+ * @param full_history the full history to hydrate with
73
+ */
74
+ hydrate(full_history: CommandNode): void;
70
75
  }
71
76
 
72
77
  /**
73
78
  * Command node interface that is used to create the undo/redo history
74
79
  */
75
- interface CommandNode {
80
+ export interface CommandNode {
76
81
  /**
77
82
  * Command that the node holds
78
83
  */
@@ -147,7 +152,14 @@ export function command_manager(): CommandManager {
147
152
  can_redo.set(!!history.next);
148
153
  current_history.set(history);
149
154
  },
150
-
155
+ hydrate: function (full_history: CommandNode) {
156
+ setTimeout(() => {
157
+ while (full_history.next) {
158
+ this.execute(full_history.next.command!);
159
+ full_history = full_history.next;
160
+ }
161
+ }, 1000);
162
+ },
151
163
  can_undo,
152
164
  can_redo,
153
165
  current_history,