@gradio/imageeditor 0.14.4 → 0.15.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/InteractiveImageEditor.svelte +4 -7
  3. package/dist/InteractiveImageEditor.svelte +4 -3
  4. package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
  5. package/dist/shared/Controls.svelte +32 -1
  6. package/dist/shared/Controls.svelte.d.ts +2 -0
  7. package/dist/shared/ImageEditor.svelte +23 -1
  8. package/dist/shared/ImageEditor.svelte.d.ts +2 -1
  9. package/dist/shared/brush/brush-cursor.js +0 -42
  10. package/dist/shared/brush/brush-textures.d.ts +19 -2
  11. package/dist/shared/brush/brush-textures.js +120 -82
  12. package/dist/shared/brush/brush.js +0 -52
  13. package/dist/shared/{utils → core}/commands.d.ts +19 -58
  14. package/dist/shared/core/commands.js +70 -0
  15. package/dist/shared/core/editor.d.ts +4 -91
  16. package/dist/shared/core/editor.js +18 -708
  17. package/dist/shared/core/layers.d.ts +143 -0
  18. package/dist/shared/core/layers.js +774 -0
  19. package/dist/shared/image/image.d.ts +14 -4
  20. package/dist/shared/image/image.js +91 -18
  21. package/dist/shared/resize/resize.d.ts +31 -0
  22. package/dist/shared/resize/resize.js +127 -290
  23. package/dist/shared/zoom/zoom.js +0 -51
  24. package/package.json +7 -7
  25. package/shared/Controls.svelte +33 -1
  26. package/shared/ImageEditor.svelte +25 -9
  27. package/shared/Toolbar.svelte +1 -4
  28. package/shared/brush/ColorSwatch.svelte +0 -3
  29. package/shared/brush/brush-cursor.ts +0 -42
  30. package/shared/brush/brush-textures.ts +166 -83
  31. package/shared/brush/brush.ts +0 -53
  32. package/shared/core/commands.ts +118 -0
  33. package/shared/core/editor.ts +33 -957
  34. package/shared/core/layers.ts +1068 -0
  35. package/shared/image/image.ts +139 -21
  36. package/shared/resize/resize.ts +160 -299
  37. package/shared/zoom/zoom.ts +0 -51
  38. package/dist/shared/utils/commands.js +0 -73
  39. package/shared/utils/commands.ts +0 -174
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @gradio/imageeditor
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Features
6
+
7
+ - [#11192](https://github.com/gradio-app/gradio/pull/11192) [`a03736f`](https://github.com/gradio-app/gradio/commit/a03736f97c642b338aa701166d3a0b299a46d27a) - Add undo and redo to the `ImageEditor` component. Thanks @pngwn!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/statustracker@0.10.11
12
+ - @gradio/atoms@0.16.1
13
+ - @gradio/client@1.15.0
14
+ - @gradio/upload@0.16.5
15
+ - @gradio/image@0.22.6
16
+
17
+ ## 0.14.5
18
+
19
+ ### Dependency updates
20
+
21
+ - @gradio/upload@0.16.4
22
+ - @gradio/image@0.22.5
23
+
3
24
  ## 0.14.4
4
25
 
5
26
  ### Dependency updates
@@ -16,18 +16,14 @@
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
- import { type CommandNode } from "./shared/utils/commands";
19
+ import { type CommandNode } from "./shared/core/commands";
20
20
  import ImageEditor from "./shared/ImageEditor.svelte";
21
- // import Layers from "./layers/Layers.svelte";
22
21
  import { type Brush as IBrush, type Eraser } from "./shared/brush/types";
23
- // import { type Eraser } from "./tools/Brush.svelte";
24
22
  import { type Tool as ToolbarTool } from "./shared/Toolbar.svelte";
25
23
 
26
- // import { Tools, Crop, Brush, Sources } from "./tools";
27
24
  import { BlockLabel } from "@gradio/atoms";
28
25
  import { Image as ImageIcon } from "@gradio/icons";
29
26
  import { inject } from "./shared/utils/parse_placeholder";
30
- // import Sources from "./shared/image/Sources.svelte";
31
27
  import {
32
28
  type LayerOptions,
33
29
  type Transform,
@@ -138,7 +134,7 @@
138
134
  $: handle_value({ layers, composite, background });
139
135
 
140
136
  let background_image = false;
141
- let history = false;
137
+ let can_undo: boolean;
142
138
 
143
139
  export let image_id: null | string = null;
144
140
 
@@ -243,8 +239,9 @@
243
239
  {webcam_options}
244
240
  {show_download_button}
245
241
  {theme_mode}
242
+ bind:can_undo
246
243
  >
247
- {#if !background_image && current_tool === "image" && !has_drawn}
244
+ {#if current_tool === "image" && !can_undo}
248
245
  <div class="empty wrap">
249
246
  {#if sources && sources.length}
250
247
  {#if heading || paragraph}
@@ -3,7 +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 "./shared/utils/commands";
6
+ import {} from "./shared/core/commands";
7
7
  import ImageEditor from "./shared/ImageEditor.svelte";
8
8
  import {} from "./shared/brush/types";
9
9
  import {} from "./shared/Toolbar.svelte";
@@ -89,7 +89,7 @@ function handle_value(value) {
89
89
  $:
90
90
  handle_value({ layers, composite, background });
91
91
  let background_image = false;
92
- let history = false;
92
+ let can_undo;
93
93
  export let image_id = null;
94
94
  function nextframe() {
95
95
  return new Promise((resolve) => setTimeout(() => resolve(), 30));
@@ -189,8 +189,9 @@ let current_tool;
189
189
  {webcam_options}
190
190
  {show_download_button}
191
191
  {theme_mode}
192
+ bind:can_undo
192
193
  >
193
- {#if !background_image && current_tool === "image" && !has_drawn}
194
+ {#if current_tool === "image" && !can_undo}
194
195
  <div class="empty wrap">
195
196
  {#if sources && sources.length}
196
197
  {#if heading || paragraph}
@@ -11,7 +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 "./shared/utils/commands";
14
+ import { type CommandNode } from "./shared/core/commands";
15
15
  import { type Brush as IBrush, type Eraser } from "./shared/brush/types";
16
16
  import { type LayerOptions, type Transform, type Source, type WebcamOptions } from "./shared/types";
17
17
  declare const __propDef: {
@@ -1,12 +1,23 @@
1
1
  <script>import { createEventDispatcher } from "svelte";
2
2
  import { IconButton, IconButtonWrapper } from "@gradio/atoms";
3
- import { Check, Trash, ZoomIn, ZoomOut, Pan, Download } from "@gradio/icons";
3
+ import {
4
+ Check,
5
+ Trash,
6
+ ZoomIn,
7
+ ZoomOut,
8
+ Pan,
9
+ Download,
10
+ Undo,
11
+ Redo
12
+ } from "@gradio/icons";
4
13
  export let can_save = false;
5
14
  export let changeable = false;
6
15
  export let current_zoom = 1;
7
16
  export let tool;
8
17
  export let min_zoom = true;
9
18
  export let enable_download = false;
19
+ export let can_undo;
20
+ export let can_redo;
10
21
  const dispatch = createEventDispatcher();
11
22
  let show_zoom_popup = false;
12
23
  function handle_zoom_click(e) {
@@ -97,6 +108,26 @@ $:
97
108
  </div>
98
109
  <div class="separator"></div>
99
110
 
111
+ <IconButton
112
+ Icon={Undo}
113
+ label="Undo"
114
+ on:click={(event) => {
115
+ dispatch("undo");
116
+ event.stopPropagation();
117
+ }}
118
+ disabled={!can_undo}
119
+ />
120
+
121
+ <IconButton
122
+ Icon={Redo}
123
+ label="Redo"
124
+ on:click={(event) => {
125
+ dispatch("redo");
126
+ event.stopPropagation();
127
+ }}
128
+ disabled={!can_redo}
129
+ />
130
+
100
131
  {#if changeable}
101
132
  <IconButton
102
133
  disabled={!can_save}
@@ -7,6 +7,8 @@ declare const __propDef: {
7
7
  tool: string;
8
8
  min_zoom?: boolean | undefined;
9
9
  enable_download?: boolean | undefined;
10
+ can_undo: boolean;
11
+ can_redo: boolean;
10
12
  };
11
13
  events: {
12
14
  remove_image: CustomEvent<void>;
@@ -8,7 +8,7 @@ import { ResizeTool } from "./resize/resize";
8
8
  import { Webcam } from "@gradio/image";
9
9
  import tinycolor, {} from "tinycolor2";
10
10
  import { ZoomTool } from "./zoom/zoom";
11
- import {} from "./utils/commands";
11
+ import {} from "./core/commands";
12
12
  import { ImageEditor } from "./core/editor";
13
13
  import {} from "./brush/types";
14
14
  import { BrushTool } from "./brush/brush";
@@ -163,6 +163,8 @@ onMount(() => {
163
163
  });
164
164
  let crop;
165
165
  let crop_zoom;
166
+ export let can_undo = false;
167
+ let can_redo = false;
166
168
  async function init_image_editor() {
167
169
  brush = new BrushTool();
168
170
  zoom = new ZoomTool();
@@ -211,6 +213,8 @@ async function init_image_editor() {
211
213
  });
212
214
  editor.on("change", () => {
213
215
  dispatch("change");
216
+ can_undo = editor.command_manager.history.previous !== null;
217
+ can_redo = editor.command_manager.history.next !== null;
214
218
  });
215
219
  if (background || layers.length > 0) {
216
220
  if (background) {
@@ -248,7 +252,11 @@ async function handle_files(files) {
248
252
  dispatch("upload");
249
253
  dispatch("input");
250
254
  dispatch("change");
255
+ can_undo = editor.command_manager.history.previous !== null;
256
+ can_redo = editor.command_manager.history.next !== null;
251
257
  }
258
+ $:
259
+ background_image = can_undo && editor.command_manager.contains("AddImage");
252
260
  function handle_tool_change({ tool }) {
253
261
  editor.set_tool(tool);
254
262
  current_tool = tool;
@@ -408,6 +416,16 @@ async function handle_download() {
408
416
  link.click();
409
417
  URL.revokeObjectURL(url);
410
418
  }
419
+ function handle_undo() {
420
+ editor.undo();
421
+ can_undo = editor.command_manager.history.previous !== null;
422
+ can_redo = editor.command_manager.history.next !== null;
423
+ }
424
+ function handle_redo() {
425
+ editor.redo();
426
+ can_undo = editor.command_manager.history.previous !== null;
427
+ can_redo = editor.command_manager.history.next !== null;
428
+ }
411
429
  </script>
412
430
 
413
431
  <div
@@ -447,6 +465,10 @@ async function handle_download() {
447
465
  }}
448
466
  enable_download={show_download_button}
449
467
  on:download={() => handle_download()}
468
+ {can_undo}
469
+ {can_redo}
470
+ on:undo={handle_undo}
471
+ on:redo={handle_redo}
450
472
  />
451
473
  {/if}
452
474
 
@@ -4,7 +4,7 @@ export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
4
4
  import { type Tool as ToolbarTool } from "./Toolbar.svelte";
5
5
  import type { I18nFormatter } from "@gradio/utils";
6
6
  import type { Client, FileData } from "@gradio/client";
7
- import { type CommandNode } from "./utils/commands";
7
+ import { type CommandNode } from "./core/commands";
8
8
  import { type Brush, type Eraser } from "./brush/types";
9
9
  import type { LayerOptions, Source, Transform, WebcamOptions } from "./types";
10
10
  import { type ImageBlobs } from "./types";
@@ -43,6 +43,7 @@ declare const __propDef: {
43
43
  };
44
44
  } | any) => Promise<void>) | undefined;
45
45
  add_layers_from_url?: ((source: FileData[] | any) => Promise<void>) | undefined;
46
+ can_undo?: boolean | undefined;
46
47
  };
47
48
  events: {
48
49
  error: CustomEvent<string>;
@@ -10,7 +10,6 @@ export class BrushCursor {
10
10
  image_editor_context;
11
11
  state;
12
12
  scale;
13
- // Cursor-related properties
14
13
  cursor_graphics = null;
15
14
  cursor_container = null;
16
15
  brush_preview_container = null;
@@ -19,7 +18,6 @@ export class BrushCursor {
19
18
  is_cursor_over_image = false;
20
19
  cursor_position_check_timeout = null;
21
20
  is_brush_or_erase_active = false;
22
- // Event handlers
23
21
  _bound_update_cursor = null;
24
22
  _bound_check_cursor_over_image = null;
25
23
  constructor(image_editor_context, state, scale) {
@@ -39,18 +37,13 @@ export class BrushCursor {
39
37
  * Sets up cursor-related event listeners.
40
38
  */
41
39
  setup_event_listeners() {
42
- // Clean up any existing listeners
43
40
  this.cleanup_event_listeners();
44
- // Create bound event handlers
45
41
  this._bound_update_cursor = this.update_cursor_position.bind(this);
46
42
  this._bound_check_cursor_over_image =
47
43
  this.check_cursor_over_image.bind(this);
48
- // Add event listeners using Pixi event system
49
- // Use app.stage for global events like pointermove
50
44
  const stage = this.image_editor_context.app.stage;
51
45
  stage.on("pointermove", this._bound_update_cursor);
52
46
  stage.on("pointermove", this._bound_check_cursor_over_image);
53
- // Add event listeners to the image container for enter/leave events
54
47
  this.image_editor_context.image_container.on("pointerenter", this.on_image_container_pointer_enter.bind(this));
55
48
  this.image_editor_context.image_container.on("pointerleave", this.on_image_container_pointer_leave.bind(this));
56
49
  }
@@ -58,9 +51,7 @@ export class BrushCursor {
58
51
  * Removes all cursor-related event listeners.
59
52
  */
60
53
  cleanup_event_listeners() {
61
- // Use Pixi event system for removal
62
54
  const stage = this.image_editor_context.app.stage;
63
- // Remove event listeners if they exist
64
55
  if (this._bound_update_cursor) {
65
56
  stage.off("pointermove", this._bound_update_cursor);
66
57
  this._bound_update_cursor = null;
@@ -69,17 +60,14 @@ export class BrushCursor {
69
60
  stage.off("pointermove", this._bound_check_cursor_over_image);
70
61
  this._bound_check_cursor_over_image = null;
71
62
  }
72
- // Remove image container event listeners
73
63
  this.image_editor_context.image_container.off("pointerenter");
74
64
  this.image_editor_context.image_container.off("pointerleave");
75
- // Clear timeout
76
65
  this.cursor_position_check_timeout = clear_timeout(this.cursor_position_check_timeout);
77
66
  }
78
67
  /**
79
68
  * initializes the cursor for the brush tool.
80
69
  */
81
70
  initialize_cursor() {
82
- // clean up existing cursor if it exists
83
71
  if (this.cursor_container) {
84
72
  if (this.cursor_container.parent) {
85
73
  this.cursor_container.parent.removeChild(this.cursor_container);
@@ -87,13 +75,10 @@ export class BrushCursor {
87
75
  this.cursor_container.destroy({ children: true });
88
76
  this.cursor_container = null;
89
77
  }
90
- // create new cursor container and add to ui container
91
78
  this.cursor_container = new Container();
92
79
  this.image_editor_context.ui_container.addChild(this.cursor_container);
93
- // create cursor graphics
94
80
  this.cursor_graphics = new Graphics();
95
81
  this.cursor_container.addChild(this.cursor_graphics);
96
- // draw initial cursor
97
82
  this.update_cursor_appearance();
98
83
  }
99
84
  /**
@@ -102,13 +87,10 @@ export class BrushCursor {
102
87
  update_cursor_appearance() {
103
88
  if (!this.cursor_graphics)
104
89
  return;
105
- // clear previous graphics
106
90
  this.cursor_graphics.clear();
107
- // set color based on mode
108
91
  const cursor_color = this.state.mode === "draw"
109
92
  ? tinycolor(this.state.color).toString()
110
93
  : 0xffffff;
111
- // draw circle with current brush size
112
94
  this.cursor_graphics
113
95
  .circle(0, 0, this.state.brush_size * this.scale)
114
96
  .stroke({
@@ -116,7 +98,6 @@ export class BrushCursor {
116
98
  color: cursor_color,
117
99
  alpha: 0.8
118
100
  });
119
- // add a small dot in the center for precision
120
101
  this.cursor_graphics.circle(0, 0, 1).fill({
121
102
  color: cursor_color,
122
103
  alpha: 0.8
@@ -129,11 +110,8 @@ export class BrushCursor {
129
110
  update_cursor_position(event) {
130
111
  if (!this.cursor_container)
131
112
  return;
132
- // get position in image coordinates
133
113
  const local_pos = this.image_editor_context.image_container.toLocal(event.global);
134
- // convert to ui coordinates
135
114
  const ui_pos = this.image_editor_context.ui_container.toLocal(this.image_editor_context.image_container.toGlobal(local_pos));
136
- // update cursor position
137
115
  this.cursor_container.position.set(ui_pos.x, ui_pos.y);
138
116
  }
139
117
  /**
@@ -141,22 +119,18 @@ export class BrushCursor {
141
119
  * @param {FederatedPointerEvent} event - The pointer event.
142
120
  */
143
121
  check_cursor_over_image(event) {
144
- // cancel any pending timeout
145
122
  if (this.cursor_position_check_timeout !== null) {
146
123
  window.clearTimeout(this.cursor_position_check_timeout);
147
124
  this.cursor_position_check_timeout = null;
148
125
  }
149
- // get the image container bounds in global coordinates
150
126
  const image_container = this.image_editor_context.image_container;
151
127
  const bounds = image_container.getBounds();
152
- // check if cursor is over image
153
128
  const was_over_image = this.is_cursor_over_image;
154
129
  this.is_cursor_over_image =
155
130
  event.global.x >= bounds.x &&
156
131
  event.global.x <= bounds.x + bounds.width &&
157
132
  event.global.y >= bounds.y &&
158
133
  event.global.y <= bounds.y + bounds.height;
159
- // update visibility if state changed
160
134
  if (was_over_image !== this.is_cursor_over_image) {
161
135
  this.update_cursor_and_preview_visibility();
162
136
  }
@@ -194,7 +168,6 @@ export class BrushCursor {
194
168
  show && this.is_brush_or_erase_active;
195
169
  }
196
170
  if (show) {
197
- // update preview appearance and position
198
171
  this.update_brush_preview();
199
172
  this.update_brush_preview_position();
200
173
  }
@@ -203,7 +176,6 @@ export class BrushCursor {
203
176
  * initializes the brush preview.
204
177
  */
205
178
  initialize_brush_preview() {
206
- // clean up existing preview if it exists
207
179
  if (this.brush_preview_container) {
208
180
  if (this.brush_preview_container.parent) {
209
181
  this.brush_preview_container.parent.removeChild(this.brush_preview_container);
@@ -211,13 +183,10 @@ export class BrushCursor {
211
183
  this.brush_preview_container.destroy({ children: true });
212
184
  this.brush_preview_container = null;
213
185
  }
214
- // create new preview container and add to ui container
215
186
  this.brush_preview_container = new Container();
216
187
  this.image_editor_context.ui_container.addChild(this.brush_preview_container);
217
- // create preview graphics
218
188
  this.brush_preview_graphics = new Graphics();
219
189
  this.brush_preview_container.addChild(this.brush_preview_graphics);
220
- // set initial visibility
221
190
  this.brush_preview_container.visible = false;
222
191
  }
223
192
  /**
@@ -226,19 +195,15 @@ export class BrushCursor {
226
195
  update_brush_preview() {
227
196
  if (!this.brush_preview_graphics)
228
197
  return;
229
- // clear previous graphics
230
198
  this.brush_preview_graphics.clear();
231
- // draw brush preview based on current settings
232
199
  const preview_size = this.state.brush_size * this.scale;
233
200
  const preview_color = this.state.mode === "draw"
234
201
  ? tinycolor(this.state.color).setAlpha(this.state.opacity).toString()
235
202
  : 0xffffff;
236
- // draw circle with current brush settings
237
203
  this.brush_preview_graphics.circle(0, 0, preview_size).fill({
238
204
  color: preview_color,
239
205
  alpha: this.state.mode === "draw" ? this.state.opacity : 0.3
240
206
  });
241
- // add outline
242
207
  this.brush_preview_graphics.circle(0, 0, preview_size + 1).stroke({
243
208
  width: 1,
244
209
  color: 0x000000,
@@ -251,14 +216,11 @@ export class BrushCursor {
251
216
  update_brush_preview_position() {
252
217
  if (!this.brush_preview_container)
253
218
  return;
254
- // get the center of the image in global coordinates
255
219
  const image_container = this.image_editor_context.image_container;
256
220
  const center_x = image_container.width / 2;
257
221
  const center_y = image_container.height / 2;
258
222
  const global_pos = image_container.toGlobal({ x: center_x, y: center_y });
259
- // convert to ui coordinates
260
223
  const ui_pos = this.image_editor_context.ui_container.toLocal(global_pos);
261
- // update preview position
262
224
  this.brush_preview_container.position.set(ui_pos.x, ui_pos.y);
263
225
  }
264
226
  /**
@@ -289,9 +251,7 @@ export class BrushCursor {
289
251
  * Cleans up all cursor resources.
290
252
  */
291
253
  cleanup() {
292
- // Reset active state
293
254
  this.is_brush_or_erase_active = false;
294
- // clean up cursor
295
255
  if (this.cursor_container) {
296
256
  if (this.cursor_container.parent) {
297
257
  this.cursor_container.parent.removeChild(this.cursor_container);
@@ -299,7 +259,6 @@ export class BrushCursor {
299
259
  this.cursor_container.destroy({ children: true });
300
260
  this.cursor_container = null;
301
261
  }
302
- // clean up preview
303
262
  if (this.brush_preview_container) {
304
263
  if (this.brush_preview_container.parent) {
305
264
  this.brush_preview_container.parent.removeChild(this.brush_preview_container);
@@ -307,7 +266,6 @@ export class BrushCursor {
307
266
  this.brush_preview_container.destroy({ children: true });
308
267
  this.brush_preview_container = null;
309
268
  }
310
- // clear timeout
311
269
  if (this.cursor_position_check_timeout !== null) {
312
270
  window.clearTimeout(this.cursor_position_check_timeout);
313
271
  this.cursor_position_check_timeout = null;
@@ -1,5 +1,20 @@
1
- import { Application } from "pixi.js";
1
+ import { Application, Texture } from "pixi.js";
2
2
  import { type ImageEditorContext } from "../core/editor";
3
+ import { type Command } from "../core/commands";
4
+ export declare class BrushCommand implements Command {
5
+ private context;
6
+ private layer_id;
7
+ private original_texture;
8
+ private final_texture;
9
+ name: string;
10
+ constructor(context: ImageEditorContext, layer_id: string, original_texture: Texture, final_texture: Texture);
11
+ /**
12
+ * Creates a new texture with the same content as the source texture
13
+ */
14
+ private createTextureFrom;
15
+ execute(): Promise<void>;
16
+ undo(): Promise<void>;
17
+ }
3
18
  /**
4
19
  * Class to handle texture operations for the brush tool.
5
20
  * Simplified to work directly with layer textures.
@@ -18,6 +33,8 @@ export declare class BrushTextures {
18
33
  private is_new_stroke;
19
34
  private current_opacity;
20
35
  private current_mode;
36
+ private original_layer_texture;
37
+ private active_layer_id;
21
38
  constructor(image_editor_context: ImageEditorContext, app: Application);
22
39
  /**
23
40
  * Initializes textures needed for the brush tool.
@@ -40,7 +57,7 @@ export declare class BrushTextures {
40
57
  */
41
58
  reset_eraser_mask(): void;
42
59
  /**
43
- * Commits the current stroke to the active layer.
60
+ * Commits the current stroke to the active layer and returns a command for undo/redo.
44
61
  */
45
62
  commit_stroke(): void;
46
63
  /**