@gradio/imageeditor 0.14.5 → 0.15.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -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
@@ -1,5 +1,62 @@
1
- import { Container, RenderTexture, Sprite, Graphics, Application } from "pixi.js";
1
+ import { Container, RenderTexture, Sprite, Graphics, Application, Texture } from "pixi.js";
2
2
  import {} from "../core/editor";
3
+ import {} from "../core/commands";
4
+ export class BrushCommand {
5
+ context;
6
+ layer_id;
7
+ original_texture;
8
+ final_texture;
9
+ name;
10
+ constructor(context, layer_id, original_texture, final_texture) {
11
+ this.context = context;
12
+ this.name = "Draw";
13
+ this.layer_id = layer_id;
14
+ this.original_texture = this.createTextureFrom(original_texture);
15
+ this.final_texture = this.createTextureFrom(final_texture);
16
+ }
17
+ /**
18
+ * Creates a new texture with the same content as the source texture
19
+ */
20
+ createTextureFrom(source) {
21
+ const texture = RenderTexture.create({
22
+ width: source.width,
23
+ height: source.height,
24
+ resolution: window.devicePixelRatio || 1
25
+ });
26
+ const sprite = new Sprite(source);
27
+ const container = new Container();
28
+ container.addChild(sprite);
29
+ this.context.app.renderer.render(container, {
30
+ renderTexture: texture
31
+ });
32
+ container.destroy({ children: true });
33
+ return texture;
34
+ }
35
+ async execute() {
36
+ const layer_textures = this.context.layer_manager.get_layer_textures(this.layer_id);
37
+ if (!layer_textures)
38
+ return;
39
+ const temp_sprite = new Sprite(this.final_texture);
40
+ const temp_container = new Container();
41
+ temp_container.addChild(temp_sprite);
42
+ this.context.app.renderer.render(temp_container, {
43
+ renderTexture: layer_textures.draw
44
+ });
45
+ temp_container.destroy({ children: true });
46
+ }
47
+ async undo() {
48
+ const layer_textures = this.context.layer_manager.get_layer_textures(this.layer_id);
49
+ if (!layer_textures)
50
+ return;
51
+ const temp_sprite = new Sprite(this.original_texture);
52
+ const temp_container = new Container();
53
+ temp_container.addChild(temp_sprite);
54
+ this.context.app.renderer.render(temp_container, {
55
+ renderTexture: layer_textures.draw
56
+ });
57
+ temp_container.destroy({ children: true });
58
+ }
59
+ }
3
60
  /**
4
61
  * Class to handle texture operations for the brush tool.
5
62
  * Simplified to work directly with layer textures.
@@ -7,7 +64,6 @@ import {} from "../core/editor";
7
64
  export class BrushTextures {
8
65
  image_editor_context;
9
66
  app;
10
- // Only keep what we need
11
67
  stroke_texture = null;
12
68
  erase_texture = null;
13
69
  display_container = null;
@@ -16,12 +72,11 @@ export class BrushTextures {
16
72
  preview_sprite = null;
17
73
  erase_graphics = null;
18
74
  dimensions;
19
- // Track if a new stroke is starting
20
75
  is_new_stroke = true;
21
- // Track the current drawing opacity
22
76
  current_opacity = 1.0;
23
- // Track current drawing mode
24
77
  current_mode = "draw";
78
+ original_layer_texture = null;
79
+ active_layer_id = null;
25
80
  constructor(image_editor_context, app) {
26
81
  this.image_editor_context = image_editor_context;
27
82
  this.app = app;
@@ -34,29 +89,23 @@ export class BrushTextures {
34
89
  * Initializes textures needed for the brush tool.
35
90
  */
36
91
  initialize_textures() {
37
- // Clean up any existing resources
38
92
  this.cleanup_textures();
39
- // Get dimensions from the image container
40
93
  const local_bounds = this.image_editor_context.image_container.getLocalBounds();
41
94
  this.dimensions = {
42
95
  width: local_bounds.width,
43
96
  height: local_bounds.height
44
97
  };
45
- // Create a texture for the current stroke preview
46
98
  this.stroke_texture = RenderTexture.create({
47
99
  width: this.dimensions.width,
48
100
  height: this.dimensions.height,
49
101
  resolution: window.devicePixelRatio || 1
50
102
  });
51
- // Create a texture for erasing
52
103
  this.erase_texture = RenderTexture.create({
53
104
  width: this.dimensions.width,
54
105
  height: this.dimensions.height,
55
106
  resolution: window.devicePixelRatio || 1
56
107
  });
57
- // Create a container to display preview
58
108
  this.display_container = new Container();
59
- // Get the active layer and add the display container to it
60
109
  const active_layer = this.image_editor_context.layer_manager.get_active_layer();
61
110
  if (active_layer) {
62
111
  active_layer.addChild(this.display_container);
@@ -64,18 +113,13 @@ export class BrushTextures {
64
113
  else {
65
114
  this.image_editor_context.image_container.addChild(this.display_container);
66
115
  }
67
- // Create container for the stroke graphics
68
116
  this.stroke_container = new Container();
69
117
  this.stroke_graphics = new Graphics();
70
118
  this.stroke_container.addChild(this.stroke_graphics);
71
- // Create graphics for eraser mask
72
119
  this.erase_graphics = new Graphics();
73
- // Create a sprite for the preview of the current stroke
74
120
  this.preview_sprite = new Sprite(this.stroke_texture);
75
- // Start with the preview sprite invisible
76
121
  this.preview_sprite.alpha = 0;
77
122
  this.display_container.addChild(this.preview_sprite);
78
- // Clear the stroke texture
79
123
  const clear_container = new Container();
80
124
  this.app.renderer.render(clear_container, {
81
125
  renderTexture: this.stroke_texture
@@ -84,7 +128,6 @@ export class BrushTextures {
84
128
  renderTexture: this.erase_texture
85
129
  });
86
130
  clear_container.destroy();
87
- // Reset drawing state
88
131
  this.is_new_stroke = true;
89
132
  this.current_opacity = 1.0;
90
133
  }
@@ -103,17 +146,14 @@ export class BrushTextures {
103
146
  * Cleans up texture resources.
104
147
  */
105
148
  cleanup_textures() {
106
- // Clean up stroke texture
107
149
  if (this.stroke_texture) {
108
150
  this.stroke_texture.destroy();
109
151
  this.stroke_texture = null;
110
152
  }
111
- // Clean up erase texture
112
153
  if (this.erase_texture) {
113
154
  this.erase_texture.destroy();
114
155
  this.erase_texture = null;
115
156
  }
116
- // Clean up display container
117
157
  if (this.display_container) {
118
158
  if (this.display_container.parent) {
119
159
  this.display_container.parent.removeChild(this.display_container);
@@ -121,17 +161,46 @@ export class BrushTextures {
121
161
  this.display_container.destroy({ children: true });
122
162
  this.display_container = null;
123
163
  }
124
- // Nullify other references
164
+ if (this.original_layer_texture) {
165
+ this.original_layer_texture.destroy();
166
+ this.original_layer_texture = null;
167
+ }
125
168
  this.stroke_container = null;
126
169
  this.stroke_graphics = null;
127
170
  this.preview_sprite = null;
128
171
  this.erase_graphics = null;
172
+ this.active_layer_id = null;
129
173
  }
130
174
  /**
131
175
  * Preserve the canvas state when starting a new stroke.
132
176
  */
133
177
  preserve_canvas_state() {
134
- // Reset for a new stroke
178
+ const active_layer = this.image_editor_context.layer_manager.get_active_layer();
179
+ if (!active_layer)
180
+ return;
181
+ const layers = this.image_editor_context.layer_manager.get_layers();
182
+ const layer = layers.find((l) => l.container === active_layer);
183
+ if (!layer)
184
+ return;
185
+ this.active_layer_id = layer.id;
186
+ const layer_textures = this.image_editor_context.layer_manager.get_layer_textures(layer.id);
187
+ if (!layer_textures)
188
+ return;
189
+ if (this.original_layer_texture) {
190
+ this.original_layer_texture.destroy();
191
+ }
192
+ this.original_layer_texture = RenderTexture.create({
193
+ width: this.dimensions.width,
194
+ height: this.dimensions.height,
195
+ resolution: window.devicePixelRatio || 1
196
+ });
197
+ const temp_sprite = new Sprite(layer_textures.draw);
198
+ const temp_container = new Container();
199
+ temp_container.addChild(temp_sprite);
200
+ this.app.renderer.render(temp_container, {
201
+ renderTexture: this.original_layer_texture
202
+ });
203
+ temp_container.destroy({ children: true });
135
204
  this.is_new_stroke = true;
136
205
  }
137
206
  /**
@@ -140,79 +209,76 @@ export class BrushTextures {
140
209
  reset_eraser_mask() {
141
210
  if (!this.erase_graphics || !this.erase_texture)
142
211
  return;
143
- // Clear and reset the eraser graphics
144
212
  this.erase_graphics.clear();
145
213
  this.erase_graphics.beginFill(0xffffff, 1);
146
214
  this.erase_graphics.drawRect(0, 0, this.dimensions.width, this.dimensions.height);
147
215
  this.erase_graphics.endFill();
148
- // Render the eraser graphics to the erase texture
149
216
  this.app.renderer.render(this.erase_graphics, {
150
217
  renderTexture: this.erase_texture
151
218
  });
152
219
  }
153
220
  /**
154
- * Commits the current stroke to the active layer.
221
+ * Commits the current stroke to the active layer and returns a command for undo/redo.
155
222
  */
156
223
  commit_stroke() {
157
- if (!this.stroke_texture || !this.preview_sprite || !this.stroke_graphics)
224
+ if (!this.stroke_texture ||
225
+ !this.preview_sprite ||
226
+ !this.stroke_graphics ||
227
+ !this.original_layer_texture ||
228
+ !this.active_layer_id)
158
229
  return;
159
- // Hide the preview sprite
160
230
  this.preview_sprite.visible = false;
161
- // Get the active layer's texture
162
231
  const active_layer = this.image_editor_context.layer_manager.get_active_layer();
163
232
  if (!active_layer)
164
233
  return;
165
- // Get all layers to find the ID of the active layer
166
234
  const layers = this.image_editor_context.layer_manager.get_layers();
167
235
  const layer = layers.find((l) => l.container === active_layer);
168
236
  if (!layer)
169
237
  return;
170
- // Get the active layer's draw texture
238
+ if (layer.id !== this.active_layer_id)
239
+ return;
171
240
  const layer_textures = this.image_editor_context.layer_manager.get_layer_textures(layer.id);
172
241
  if (!layer_textures)
173
242
  return;
174
- // Save the current opacity for use in committing
175
243
  const currentOpacity = this.current_opacity;
176
- // Handle drawing or erasing
177
244
  if (this.current_mode === "draw") {
178
- // Drawing mode - simple composition
179
245
  const temp_container = new Container();
180
- // Add current layer texture as base
181
246
  const base_sprite = new Sprite(layer_textures.draw);
182
247
  temp_container.addChild(base_sprite);
183
- // Add the current stroke with proper opacity
184
248
  const stroke_sprite = new Sprite(this.stroke_texture);
185
249
  stroke_sprite.alpha = currentOpacity;
186
250
  temp_container.addChild(stroke_sprite);
187
- // Render the composited result directly to the layer texture
188
251
  this.app.renderer.render(temp_container, {
189
252
  renderTexture: layer_textures.draw
190
253
  });
191
- // Clean up
192
254
  temp_container.destroy({ children: true });
193
255
  }
194
256
  else {
195
- // Erasing mode - use proper masking with setMask
196
257
  if (!this.stroke_texture)
197
258
  return;
198
- // Create a container for the erase operation
199
259
  const erase_container = new Container();
200
- // Add the current layer content
201
260
  const content_sprite = new Sprite(layer_textures.draw);
202
261
  erase_container.addChild(content_sprite);
203
- // Create the mask sprite from our stroke texture
204
262
  const mask_sprite = new Sprite(this.stroke_texture);
205
- // Apply the mask to the container using setMask with inverse: true
206
- // This will only show content where the mask is NOT (inverse: true)
207
263
  erase_container.setMask({ mask: mask_sprite, inverse: true });
208
- // Render the masked content back to layer texture
209
264
  this.app.renderer.render(erase_container, {
210
265
  renderTexture: layer_textures.draw
211
266
  });
212
- // Clean up
213
267
  erase_container.destroy({ children: true });
214
268
  }
215
- // Clear the stroke graphics and texture for the next stroke
269
+ const final_texture = RenderTexture.create({
270
+ width: this.dimensions.width,
271
+ height: this.dimensions.height,
272
+ resolution: window.devicePixelRatio || 1
273
+ });
274
+ const final_container = new Container();
275
+ const final_sprite = new Sprite(layer_textures.draw);
276
+ final_container.addChild(final_sprite);
277
+ this.app.renderer.render(final_container, {
278
+ renderTexture: final_texture
279
+ });
280
+ final_container.destroy({ children: true });
281
+ const brush_command = new BrushCommand(this.image_editor_context, this.active_layer_id, this.original_layer_texture, final_texture);
216
282
  if (this.stroke_graphics) {
217
283
  this.stroke_graphics.clear();
218
284
  }
@@ -221,8 +287,11 @@ export class BrushTextures {
221
287
  renderTexture: this.stroke_texture
222
288
  });
223
289
  clear_container.destroy();
224
- // Reset state for the next stroke
225
290
  this.is_new_stroke = true;
291
+ final_texture.destroy();
292
+ this.original_layer_texture = null;
293
+ this.active_layer_id = null;
294
+ this.image_editor_context.command_manager.execute(brush_command);
226
295
  }
227
296
  /**
228
297
  * Calculates the distance between two points.
@@ -239,17 +308,15 @@ export class BrushTextures {
239
308
  !this.stroke_container ||
240
309
  !this.preview_sprite)
241
310
  return;
242
- // Store the current mode and opacity
311
+ if (this.is_new_stroke && !this.original_layer_texture) {
312
+ this.preserve_canvas_state();
313
+ }
243
314
  this.current_mode = mode;
244
315
  this.current_opacity =
245
316
  mode === "draw" ? Math.min(Math.max(opacity, 0), 1) : 0.5;
246
- // Apply scale to brush size for consistent rendering
247
317
  const scaled_size = size;
248
- // For new strokes, clear the graphics and texture completely
249
318
  if (this.is_new_stroke) {
250
- // Clean up for a new stroke
251
319
  this.stroke_graphics.clear();
252
- // Clear the stroke texture
253
320
  const clear_container = new Container();
254
321
  this.app.renderer.render(clear_container, {
255
322
  renderTexture: this.stroke_texture
@@ -257,18 +324,14 @@ export class BrushTextures {
257
324
  clear_container.destroy();
258
325
  this.is_new_stroke = false;
259
326
  }
260
- // Set fill color and opacity for drawing
261
327
  if (mode === "draw") {
262
- // For drawing, use the specified color, but always full opacity in the graphics
263
- // We need to convert the color string to a number
264
- let colorValue = 0xffffff; // Default to white
328
+ let colorValue = 0xffffff;
265
329
  try {
266
330
  if (color.startsWith("#")) {
267
331
  colorValue = parseInt(color.replace("#", "0x"), 16);
268
332
  }
269
333
  }
270
334
  catch (e) {
271
- // Default to white if there's an error
272
335
  colorValue = 0xffffff;
273
336
  }
274
337
  this.stroke_graphics.setFillStyle({
@@ -277,50 +340,35 @@ export class BrushTextures {
277
340
  });
278
341
  }
279
342
  else {
280
- // For erasing, always use white
281
343
  this.stroke_graphics.setFillStyle({
282
344
  color: 0xffffff,
283
345
  alpha: 1.0
284
346
  });
285
347
  }
286
- // Calculate the distance between the points
287
348
  const distance = this.calculateDistance(from_x, from_y, to_x, to_y);
288
- // If the points are the same, just draw a single circle
289
349
  if (distance < 0.1) {
290
- // Draw a circle at the point - use scaled_size as RADIUS
291
350
  this.stroke_graphics.circle(from_x, from_y, scaled_size).fill();
292
351
  }
293
352
  else {
294
- // Calculate how many circles to draw along the path
295
- // Adaptive spacing that balances quality and performance
296
353
  const spacing = Math.max(scaled_size / 3, 2);
297
354
  const steps = Math.max(Math.ceil(distance / spacing), 2);
298
- // Draw a circle at each point along the path
299
355
  for (let i = 0; i < steps; i++) {
300
356
  const t = i / (steps - 1);
301
357
  const x = from_x + (to_x - from_x) * t;
302
358
  const y = from_y + (to_y - from_y) * t;
303
- // Create a circle at this point - use scaled_size as RADIUS
304
359
  this.stroke_graphics.circle(x, y, scaled_size).fill();
305
360
  }
306
361
  }
307
- // End the fill after all circles are drawn
308
362
  this.stroke_graphics.endFill();
309
- // Render the stroke to the texture
310
363
  this.app.renderer.render(this.stroke_container, {
311
364
  renderTexture: this.stroke_texture
312
365
  });
313
- // Update the preview based on mode
314
366
  if (mode === "draw") {
315
- // For drawing, show the stroke directly with proper opacity
316
367
  this.preview_sprite.texture = this.stroke_texture;
317
368
  this.preview_sprite.alpha = this.current_opacity;
318
- // Set tint to white (no tint) instead of trying to tint the texture
319
369
  this.preview_sprite.tint = 0xffffff;
320
370
  }
321
371
  else {
322
- // For erasing, create a preview that shows what will be erased
323
- // Get the active layer's texture for preview
324
372
  const active_layer = this.image_editor_context.layer_manager.get_active_layer();
325
373
  if (!active_layer)
326
374
  return;
@@ -331,13 +379,11 @@ export class BrushTextures {
331
379
  const layer_textures = this.image_editor_context.layer_manager.get_layer_textures(layer.id);
332
380
  if (!layer_textures)
333
381
  return;
334
- // Create a temporary texture for the preview
335
382
  const preview_texture = RenderTexture.create({
336
383
  width: this.dimensions.width,
337
384
  height: this.dimensions.height,
338
385
  resolution: window.devicePixelRatio || 1
339
386
  });
340
- // First, create a copy of the current layer content
341
387
  const base_container = new Container();
342
388
  const content_sprite = new Sprite(layer_textures.draw);
343
389
  base_container.addChild(content_sprite);
@@ -345,26 +391,18 @@ export class BrushTextures {
345
391
  renderTexture: preview_texture
346
392
  });
347
393
  base_container.destroy({ children: true });
348
- // Create a container for the eraser preview with red overlay
349
394
  const preview_container = new Container();
350
- // Add our current stroke as the content
351
395
  const mask_sprite = new Sprite(this.stroke_texture);
352
- // Add a red overlay to show what's being erased
353
396
  const overlay = new Graphics();
354
397
  overlay.setFillStyle({ color: 0xffffff, alpha: 0.5 });
355
398
  overlay.rect(0, 0, this.dimensions.width, this.dimensions.height).fill();
356
- // Use setMask with inverse: false to only show overlay where the stroke is
357
- // We want to show the red overlay ONLY where the stroke will erase
358
399
  overlay.setMask({ mask: mask_sprite, inverse: false });
359
400
  preview_container.addChild(overlay);
360
- // Render the overlay on top of the base content
361
401
  this.app.renderer.render(preview_container, {
362
402
  renderTexture: preview_texture
363
403
  });
364
- // Update the preview sprite with our preview texture
365
404
  this.preview_sprite.texture = preview_texture;
366
405
  this.preview_sprite.alpha = 1.0;
367
- // Clean up
368
406
  preview_container.destroy({ children: true });
369
407
  preview_texture.destroy();
370
408
  }
@@ -78,36 +78,27 @@ export class BrushTool {
78
78
  * @returns {Promise<void>}
79
79
  */
80
80
  async setup(context, tool, subtool) {
81
- // Store context and tool information
82
81
  this.image_editor_context = context;
83
82
  this.current_tool = tool;
84
83
  this.current_subtool = subtool;
85
- // Set mode based on tool
86
84
  this.state.mode = tool === "erase" ? "erase" : "draw";
87
- // Set size based on mode
88
85
  if (this.state.mode === "draw") {
89
86
  this.state.brush_size = this.brush_size;
90
87
  }
91
88
  else {
92
89
  this.state.brush_size = this.eraser_size;
93
90
  }
94
- // Subscribe to scale changes
95
91
  context.scale.subscribe((scale) => {
96
92
  this.scale = scale;
97
93
  if (this.brush_cursor) {
98
94
  this.brush_cursor.update_state(this.state, this.scale);
99
95
  }
100
96
  });
101
- // Initialize brush cursor
102
97
  this.brush_cursor = new BrushCursor(this.image_editor_context, this.state, this.scale);
103
- // Set initial active state
104
98
  this.brush_cursor.set_active(tool === "draw" || tool === "erase");
105
- // Initialize brush textures
106
99
  this.brush_textures = new BrushTextures(this.image_editor_context, context.app);
107
100
  this.brush_textures.initialize_textures();
108
- // Set up event listeners
109
101
  this.setup_event_listeners();
110
- // Set cursor style for image container
111
102
  this.handle_cursors(tool);
112
103
  }
113
104
  /**
@@ -116,7 +107,6 @@ export class BrushTool {
116
107
  * @private
117
108
  */
118
109
  handle_cursors(tool) {
119
- // Set cursor style to 'none' for all children of the image container
120
110
  recurse_set_cursor(this.image_editor_context.image_container.children, "none");
121
111
  }
122
112
  /**
@@ -125,32 +115,26 @@ export class BrushTool {
125
115
  * @param {Subtool} subtool - The current subtool.
126
116
  */
127
117
  set_tool(tool, subtool) {
128
- // Update current tool and subtool
129
118
  this.current_tool = tool;
130
119
  this.current_subtool = subtool;
131
120
  if (this.current_tool !== "erase" && this.current_tool !== "draw") {
132
121
  this.commit_pending_changes();
133
122
  }
134
- // Commit any pending changes
135
- // Set brush cursor active state
136
123
  if (this.brush_cursor) {
137
124
  this.brush_cursor.set_active(tool === "draw" || tool === "erase");
138
125
  }
139
- // Update mode based on tool
140
126
  const new_mode = tool === "erase" ? "erase" : "draw";
141
127
  if (tool === "erase" || tool === "draw") {
142
128
  this.brush_textures?.initialize_textures();
143
129
  }
144
130
  if (this.state.mode !== new_mode) {
145
131
  this.state.mode = new_mode;
146
- // Update brush size based on mode
147
132
  if (this.state.mode === "draw") {
148
133
  this.state.brush_size = this.brush_size;
149
134
  }
150
135
  else {
151
136
  this.state.brush_size = this.eraser_size;
152
137
  }
153
- // Update cursor
154
138
  if (this.brush_cursor) {
155
139
  this.brush_cursor.update_state(this.state, this.scale);
156
140
  }
@@ -178,17 +162,13 @@ export class BrushTool {
178
162
  }
179
163
  if (this.brush_cursor && !this.brush_cursor.is_over_image())
180
164
  return;
181
- // Preserve the current canvas state
182
165
  if (this.brush_textures) {
183
166
  this.brush_textures.preserve_canvas_state();
184
167
  }
185
- // Get the local position
186
168
  const local_pos = this.image_editor_context.image_container.toLocal(event.global);
187
- // Start drawing
188
169
  this.is_drawing = true;
189
170
  this.last_x = local_pos.x;
190
171
  this.last_y = local_pos.y;
191
- // Draw a single point
192
172
  if (this.brush_textures) {
193
173
  this.brush_textures.draw_segment(local_pos.x, local_pos.y, local_pos.x, local_pos.y, this.state.brush_size, this.state.color, this.state.opacity, this.state.mode);
194
174
  }
@@ -199,20 +179,15 @@ export class BrushTool {
199
179
  * @private
200
180
  */
201
181
  on_pointer_move(event) {
202
- // Update cursor position
203
182
  if (this.brush_cursor) {
204
183
  this.brush_cursor.update_cursor_position(event);
205
184
  }
206
- // If not drawing, exit
207
185
  if (!this.is_drawing)
208
186
  return;
209
- // Get the local position
210
187
  const local_pos = this.image_editor_context.image_container.toLocal(event.global);
211
- // Draw line from last position to current position
212
188
  if (this.brush_textures) {
213
189
  this.brush_textures.draw_segment(this.last_x, this.last_y, local_pos.x, local_pos.y, this.state.brush_size, this.state.color, this.state.opacity, this.state.mode);
214
190
  }
215
- // Update last position
216
191
  this.last_x = local_pos.x;
217
192
  this.last_y = local_pos.y;
218
193
  }
@@ -221,12 +196,9 @@ export class BrushTool {
221
196
  * @private
222
197
  */
223
198
  on_pointer_up() {
224
- // If not drawing, exit
225
199
  if (!this.is_drawing)
226
200
  return;
227
- // Stop drawing
228
201
  this.is_drawing = false;
229
- // Commit the stroke
230
202
  if (this.brush_textures) {
231
203
  this.brush_textures.commit_stroke();
232
204
  }
@@ -237,25 +209,19 @@ export class BrushTool {
237
209
  * @private
238
210
  */
239
211
  setup_event_listeners() {
240
- // Remove existing listeners if they exist
241
212
  this.cleanup_event_listeners();
242
- // Create bound event handlers
243
213
  this._bound_pointer_down = this.on_pointer_down.bind(this);
244
214
  this._bound_pointer_move = this.on_pointer_move.bind(this);
245
215
  this._bound_pointer_up = this.on_pointer_up.bind(this);
246
- // Set interactive mode for image container
247
216
  const image_container = this.image_editor_context.image_container;
248
217
  image_container.eventMode = "static";
249
218
  image_container.interactiveChildren = true;
250
- // Setup for stage
251
219
  const stage = this.image_editor_context.app.stage;
252
220
  stage.eventMode = "static";
253
- // Add pointer events to the stage for global tracking
254
221
  stage.on("pointerdown", this._bound_pointer_down);
255
222
  stage.on("pointermove", this._bound_pointer_move);
256
223
  stage.on("pointerup", this._bound_pointer_up);
257
224
  stage.on("pointerupoutside", this._bound_pointer_up);
258
- // Set up cursor event listeners
259
225
  if (this.brush_cursor) {
260
226
  this.brush_cursor.setup_event_listeners();
261
227
  }
@@ -266,7 +232,6 @@ export class BrushTool {
266
232
  */
267
233
  cleanup_event_listeners() {
268
234
  const stage = this.image_editor_context.app.stage;
269
- // Remove event listeners if they exist
270
235
  if (this._bound_pointer_down) {
271
236
  stage.off("pointerdown", this._bound_pointer_down);
272
237
  this._bound_pointer_down = null;
@@ -280,7 +245,6 @@ export class BrushTool {
280
245
  stage.off("pointerupoutside", this._bound_pointer_up);
281
246
  this._bound_pointer_up = null;
282
247
  }
283
- // Clean up cursor event listeners
284
248
  if (this.brush_cursor) {
285
249
  this.brush_cursor.cleanup_event_listeners();
286
250
  }
@@ -290,12 +254,9 @@ export class BrushTool {
290
254
  * @param {number} size - The new brush size.
291
255
  */
292
256
  set_brush_size(size) {
293
- // Set brush size
294
257
  this.brush_size = size;
295
- // Update state if in draw mode
296
258
  if (this.state.mode === "draw") {
297
259
  this.state.brush_size = size;
298
- // Update cursor
299
260
  if (this.brush_cursor) {
300
261
  this.brush_cursor.update_state(this.state, this.scale);
301
262
  }
@@ -306,11 +267,8 @@ export class BrushTool {
306
267
  * @param {string|ColorInput} color - The new brush color.
307
268
  */
308
269
  set_brush_color(color) {
309
- // Convert to hex string
310
270
  const color_string = tinycolor(color).toHexString();
311
- // Set color
312
271
  this.state.color = color_string;
313
- // Update cursor
314
272
  if (this.brush_cursor) {
315
273
  this.brush_cursor.update_state(this.state, this.scale);
316
274
  }
@@ -320,11 +278,8 @@ export class BrushTool {
320
278
  * @param {number} opacity - The new brush opacity.
321
279
  */
322
280
  set_brush_opacity(opacity) {
323
- // Clamp opacity between 0 and 1
324
281
  const clamped_opacity = Math.max(0, Math.min(1, opacity));
325
- // Set opacity
326
282
  this.state.opacity = clamped_opacity;
327
- // Update cursor
328
283
  if (this.brush_cursor) {
329
284
  this.brush_cursor.update_state(this.state, this.scale);
330
285
  }
@@ -334,12 +289,9 @@ export class BrushTool {
334
289
  * @param {number} size - The new eraser size.
335
290
  */
336
291
  set_eraser_size(size) {
337
- // Store the new eraser size
338
292
  this.eraser_size = size;
339
- // Update state if in erase mode
340
293
  if (this.state.mode === "erase") {
341
294
  this.state.brush_size = size;
342
- // Update cursor
343
295
  if (this.brush_cursor) {
344
296
  this.brush_cursor.update_state(this.state, this.scale);
345
297
  }
@@ -365,16 +317,12 @@ export class BrushTool {
365
317
  * Cleans up resources used by the brush tool.
366
318
  */
367
319
  cleanup() {
368
- // Commit any pending changes
369
320
  this.commit_pending_changes();
370
- // Clean up event listeners
371
321
  this.cleanup_event_listeners();
372
- // Clean up brush textures
373
322
  if (this.brush_textures) {
374
323
  this.brush_textures.cleanup();
375
324
  this.brush_textures = null;
376
325
  }
377
- // Clean up brush cursor
378
326
  if (this.brush_cursor) {
379
327
  this.brush_cursor.cleanup();
380
328
  this.brush_cursor = null;