@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,672 +1,12 @@
1
- import { Application, Container, Graphics, Sprite, Filter, RenderTexture, GlProgram, defaultFilterVert, SCALE_MODES, Texture, Assets } from "pixi.js";
1
+ import { Application, Container, Graphics, Sprite, Assets } from "pixi.js";
2
2
  import { DropShadowFilter as BlurFilter } from "pixi-filters/drop-shadow";
3
3
  import { ImageTool } from "../image/image";
4
- import { AddImageCommand } from "../image/image";
5
4
  import { ZoomTool } from "../zoom/zoom";
6
5
  import { spring } from "svelte/motion";
7
- import { writable, get } from "svelte/store";
8
- import { Rectangle } from "pixi.js";
6
+ import { writable } from "svelte/store";
9
7
  import {} from "../types";
10
- import { get_canvas_blob } from "../utils/pixi";
11
- export class CommandManager {
12
- undo_stack = [];
13
- redo_stack = [];
14
- async execute(command) {
15
- await command.execute();
16
- this.undo_stack.push(command);
17
- this.redo_stack = []; // Clear redo stack when new command is executed
18
- }
19
- async undo() {
20
- const command = this.undo_stack.pop();
21
- if (command) {
22
- await command.undo();
23
- this.redo_stack.push(command);
24
- }
25
- }
26
- async redo() {
27
- const command = this.redo_stack.pop();
28
- if (command) {
29
- await command.execute();
30
- this.undo_stack.push(command);
31
- }
32
- }
33
- }
34
- export class LayerManager {
35
- layers = [];
36
- active_layer = null;
37
- active_layer_id = null;
38
- draw_textures = new Map();
39
- layer_store = writable({
40
- active_layer: "",
41
- layers: []
42
- });
43
- background_layer = null;
44
- image_container;
45
- app;
46
- fixed_canvas;
47
- dark;
48
- border_region;
49
- layer_options;
50
- constructor(image_container, app, fixed_canvas, dark, border_region, layer_options) {
51
- this.image_container = image_container;
52
- this.app = app;
53
- this.fixed_canvas = fixed_canvas;
54
- this.dark = dark;
55
- this.border_region = border_region;
56
- this.layer_options = layer_options;
57
- }
58
- toggle_layer_visibility(id) {
59
- const layer = this.layers.find((l) => l.id === id);
60
- if (layer) {
61
- layer.container.visible = !layer.container.visible;
62
- layer.visible = layer.container.visible;
63
- this.layer_store.update((state) => ({
64
- active_layer: state.active_layer,
65
- layers: this.layers
66
- }));
67
- }
68
- }
69
- create_background_layer(width, height) {
70
- if (this.background_layer) {
71
- this.background_layer.destroy();
72
- }
73
- // Create new background layer
74
- const layer = new Container();
75
- this.background_layer = layer;
76
- // Create a render texture for the background with transparency
77
- const bg_texture = RenderTexture.create({
78
- width,
79
- height,
80
- resolution: window.devicePixelRatio,
81
- antialias: true,
82
- scaleMode: SCALE_MODES.NEAREST
83
- });
84
- // Create sprite to display the texture with transparency
85
- const bg_sprite = new Sprite(bg_texture);
86
- layer.addChild(bg_sprite);
87
- // Clear the texture with transparency
88
- const clear_graphics = new Graphics();
89
- clear_graphics.clear();
90
- clear_graphics
91
- .rect(0, 0, width, height)
92
- .fill({ color: this.dark ? 0x333333 : 0xffffff, alpha: 1 });
93
- // Render with transparency
94
- this.app.renderer.render({
95
- container: clear_graphics,
96
- target: bg_texture,
97
- clear: true
98
- });
99
- this.image_container.addChild(layer);
100
- layer.zIndex = -1;
101
- this.update_layer_order();
102
- return layer;
103
- }
104
- set_layer_options(layer_options, width, height) {
105
- this.layer_options = layer_options;
106
- this.reset_layers(width, height);
107
- }
108
- /**
109
- * Creates a background layer from an image URL
110
- * @param url The URL of the image to use as background
111
- * @param width The width of the layer (if fixed_canvas is true)
112
- * @param height The height of the layer (if fixed_canvas is true)
113
- * @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
114
- * @returns A Promise that resolves to the background Container when the image is loaded
115
- */
116
- async create_background_layer_from_url(url, width, height) {
117
- const layer = this.create_background_layer(width || this.image_container.width, height || this.image_container.height);
118
- try {
119
- const texture = await Texture.from(url);
120
- const sprite = new Sprite(texture);
121
- const imageWidth = sprite.texture.width;
122
- const imageHeight = sprite.texture.height;
123
- // Calculate dimensions based on whether canvas size is fixed
124
- const containerWidth = width || this.image_container.width;
125
- const containerHeight = height || this.image_container.height;
126
- if (this.fixed_canvas) {
127
- // If fixed canvas, fit the image within the specified dimensions minus border region
128
- // Calculate aspect-preserving dimensions
129
- const effectiveContainerWidth = Math.max(containerWidth - this.border_region * 2, 10);
130
- const effectiveContainerHeight = Math.max(containerHeight - this.border_region * 2, 10);
131
- const imageAspectRatio = imageWidth / imageHeight;
132
- const containerAspectRatio = effectiveContainerWidth / effectiveContainerHeight;
133
- let finalWidth, finalHeight;
134
- let posX = this.border_region, // Start with border offset
135
- posY = this.border_region;
136
- if (imageWidth <= effectiveContainerWidth &&
137
- imageHeight <= effectiveContainerHeight) {
138
- // Image fits within container without scaling
139
- finalWidth = imageWidth;
140
- finalHeight = imageHeight;
141
- }
142
- else {
143
- if (imageAspectRatio > containerAspectRatio) {
144
- // Width is the limiting factor
145
- finalWidth = effectiveContainerWidth;
146
- finalHeight = effectiveContainerWidth / imageAspectRatio;
147
- }
148
- else {
149
- // Height is the limiting factor
150
- finalHeight = effectiveContainerHeight;
151
- finalWidth = effectiveContainerHeight * imageAspectRatio;
152
- }
153
- }
154
- // Center image within the effective container area (which already accounts for border)
155
- posX += Math.round((effectiveContainerWidth - finalWidth) / 2);
156
- posY += Math.round((effectiveContainerHeight - finalHeight) / 2);
157
- sprite.width = finalWidth;
158
- sprite.height = finalHeight;
159
- sprite.position.set(posX, posY);
160
- }
161
- else {
162
- // If not fixed canvas, use the natural dimensions of the image plus border
163
- // Position starts at the border offset
164
- sprite.position.set(this.border_region, this.border_region);
165
- // We need to re-create the background layer with the actual image dimensions plus border
166
- if (this.background_layer) {
167
- this.background_layer.destroy();
168
- }
169
- // Create new layer with image dimensions plus border on all sides
170
- const totalWidth = imageWidth + this.border_region * 2;
171
- const totalHeight = imageHeight + this.border_region * 2;
172
- const newLayer = this.create_background_layer(totalWidth, totalHeight);
173
- sprite.width = imageWidth;
174
- sprite.height = imageHeight;
175
- newLayer.addChild(sprite);
176
- return newLayer;
177
- }
178
- layer.addChild(sprite);
179
- return layer;
180
- }
181
- catch (error) {
182
- console.error("Error loading image from URL:", error);
183
- return layer;
184
- }
185
- }
186
- create_layer({ width, height, layer_name, user_created, layer_id = undefined, make_active = false }) {
187
- const layer = new Container();
188
- const _layer_id = layer_id || Math.random().toString(36).substring(2, 15);
189
- const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
190
- this.layers.push({
191
- name: _layer_name,
192
- id: _layer_id,
193
- container: layer,
194
- user_created,
195
- visible: true
196
- });
197
- this.image_container.addChild(layer);
198
- // Create a single render texture for drawing with transparency
199
- const draw_texture = RenderTexture.create({
200
- width,
201
- height,
202
- resolution: window.devicePixelRatio,
203
- antialias: true,
204
- scaleMode: SCALE_MODES.NEAREST
205
- });
206
- const canvas_sprite = new Sprite(draw_texture);
207
- layer.addChild(canvas_sprite);
208
- const clear_graphics = new Graphics();
209
- clear_graphics.clear();
210
- clear_graphics.beginFill(0, 0);
211
- clear_graphics.drawRect(0, 0, width, height);
212
- clear_graphics.endFill();
213
- this.app.renderer.render({
214
- container: clear_graphics,
215
- target: draw_texture,
216
- clear: true
217
- });
218
- // Store texture for drawing
219
- this.draw_textures.set(layer, draw_texture);
220
- this.update_layer_order();
221
- if (make_active) {
222
- this.set_active_layer(_layer_id);
223
- }
224
- this.layer_store.set({
225
- active_layer: this.active_layer_id || "",
226
- layers: this.layers
227
- });
228
- return layer;
229
- }
230
- /**
231
- * Creates a new layer with an image loaded from a URL
232
- * @param url The URL of the image to load
233
- * @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
234
- * @returns A Promise that resolves to the layer ID
235
- */
236
- async add_layer_from_url(url) {
237
- const { width, height } = this.image_container.getLocalBounds();
238
- const layer = this.create_layer({
239
- width,
240
- height,
241
- layer_name: "Layer 1",
242
- user_created: true
243
- });
244
- const layerIndex = this.layers.findIndex((l) => l.container === layer);
245
- if (layerIndex === -1) {
246
- console.error("Could not find newly created layer");
247
- return "";
248
- }
249
- const layerId = this.layers[layerIndex].id;
250
- try {
251
- const texture = await Assets.load(url);
252
- const drawTexture = this.draw_textures.get(layer);
253
- if (!drawTexture) {
254
- console.error("No draw texture found for layer");
255
- return layerId;
256
- }
257
- const sprite = new Sprite(texture);
258
- const imageWidth = sprite.width;
259
- const imageHeight = sprite.height;
260
- let posX = this.border_region;
261
- let posY = this.border_region;
262
- // Calculate effective dimensions (accounting for border)
263
- const effectiveWidth = this.fixed_canvas
264
- ? width - this.border_region * 2
265
- : width;
266
- const effectiveHeight = this.fixed_canvas
267
- ? height - this.border_region * 2
268
- : height;
269
- // If the image is smaller than the effective layer area, center it within that area
270
- if (imageWidth < effectiveWidth || imageHeight < effectiveHeight) {
271
- posX = Math.floor((effectiveWidth - imageWidth) / 2);
272
- posY = Math.floor((effectiveHeight - imageHeight) / 2);
273
- }
274
- sprite.position.set(posX, posY);
275
- // If the image is larger than the effective layer area, scale it down to fit
276
- if (imageWidth > effectiveWidth || imageHeight > effectiveHeight) {
277
- const imageAspectRatio = imageWidth / imageHeight;
278
- const areaAspectRatio = effectiveWidth / effectiveHeight;
279
- let finalWidth, finalHeight;
280
- if (imageAspectRatio > areaAspectRatio) {
281
- // Width is the limiting factor
282
- finalWidth = effectiveWidth;
283
- finalHeight = effectiveWidth / imageAspectRatio;
284
- }
285
- else {
286
- // Height is the limiting factor
287
- finalHeight = effectiveHeight;
288
- finalWidth = effectiveHeight * imageAspectRatio;
289
- }
290
- sprite.width = finalWidth;
291
- sprite.height = finalHeight;
292
- // Recalculate position to center within effective area
293
- posX =
294
- this.border_region + Math.floor((effectiveWidth - finalWidth) / 2);
295
- posY =
296
- this.border_region + Math.floor((effectiveHeight - finalHeight) / 2);
297
- sprite.position.set(posX, posY);
298
- }
299
- this.app.renderer.render(sprite, { renderTexture: drawTexture });
300
- this.set_active_layer(layerId);
301
- return layerId;
302
- }
303
- catch (error) {
304
- console.error("Error loading image from URL:", error);
305
- return layerId;
306
- }
307
- }
308
- get_active_layer() {
309
- return this.active_layer;
310
- }
311
- set_active_layer(id) {
312
- if (this.layers.some((l) => l.id === id)) {
313
- this.active_layer =
314
- this.layers.find((l) => l.id === id)?.container ||
315
- this.layers[0]?.container ||
316
- null;
317
- this.active_layer_id = id;
318
- this.layer_store.set({
319
- active_layer: id,
320
- layers: this.layers
321
- });
322
- }
323
- }
324
- get_layers() {
325
- return this.layers;
326
- }
327
- get_layer_textures(id) {
328
- const layer = this.layers.find((l) => l.id === id);
329
- if (layer) {
330
- const draw = this.draw_textures.get(layer.container);
331
- if (draw) {
332
- return { draw };
333
- }
334
- }
335
- return null;
336
- }
337
- delete_layer(id) {
338
- const index = this.layers.findIndex((l) => l.id === id);
339
- if (index > -1) {
340
- // Clean up texture
341
- const draw_texture = this.draw_textures.get(this.layers[index].container);
342
- if (draw_texture) {
343
- draw_texture.destroy();
344
- this.draw_textures.delete(this.layers[index].container);
345
- }
346
- this.layers[index].container.destroy();
347
- if (this.active_layer === this.layers[index].container) {
348
- const new_active_layer = this.layers[Math.max(0, index - 1)] || null;
349
- this.active_layer = new_active_layer?.container || null;
350
- this.active_layer_id = new_active_layer?.id || null;
351
- }
352
- this.layers = this.layers.filter((l) => l.id !== id);
353
- this.layer_store.update((_layers) => ({
354
- active_layer: _layers.active_layer === id
355
- ? this.layers[this.layers.length - 1]?.id
356
- : _layers.active_layer,
357
- layers: this.layers
358
- }));
359
- this.update_layer_order();
360
- }
361
- }
362
- update_layer_order() {
363
- // Ensure background layer always has lowest z-index
364
- if (this.background_layer) {
365
- this.background_layer.zIndex = -1;
366
- }
367
- // Update other layers starting from 0
368
- this.layers.forEach((layer, index) => {
369
- layer.container.zIndex = index;
370
- });
371
- }
372
- move_layer(id, direction) {
373
- const index = this.layers.findIndex((l) => l.id === id);
374
- if (index > -1) {
375
- const new_index = direction === "up" ? index - 1 : index + 1;
376
- this.layers = this.layers.map((l, i) => {
377
- if (i === index) {
378
- return this.layers[new_index];
379
- }
380
- if (i === new_index) {
381
- return this.layers[index];
382
- }
383
- return l;
384
- });
385
- this.update_layer_order();
386
- this.layer_store.update((_layers) => ({
387
- active_layer: id,
388
- layers: this.layers
389
- }));
390
- }
391
- }
392
- /**
393
- * Resizes all layers to a new width and height
394
- * @param newWidth The new width of the layers
395
- * @param newHeight The new height of the layers
396
- * @param scale If true, scales the layer content to fit the new dimensions. If false, keeps the content size unchanged.
397
- * @param anchor The anchor point to position the content relative to the new size
398
- */
399
- resize_all_layers(newWidth, newHeight, scale, anchor,
400
- // Add old canvas dimensions for anchor calculations when not scaling
401
- oldCanvasWidth, oldCanvasHeight) {
402
- // Create a map of the old layers by ID for reference
403
- const oldLayersById = new Map(this.layers.map((l) => [l.id, l]));
404
- const oldBackgroundLayer = this.background_layer;
405
- // Calculate offset for non-scaling resize based on anchor and dimension change
406
- const calculateOffset = () => {
407
- let offsetX = 0;
408
- let offsetY = 0;
409
- const deltaWidth = newWidth - oldCanvasWidth;
410
- const deltaHeight = newHeight - oldCanvasHeight;
411
- // Determine X offset based on anchor point
412
- if (anchor.includes("left")) {
413
- offsetX = 0;
414
- }
415
- else if (anchor.includes("right")) {
416
- offsetX = deltaWidth;
417
- }
418
- else {
419
- // center
420
- offsetX = Math.floor(deltaWidth / 2);
421
- }
422
- // Determine Y offset based on anchor point
423
- if (anchor.includes("top")) {
424
- offsetY = 0;
425
- }
426
- else if (anchor.includes("bottom")) {
427
- offsetY = deltaHeight;
428
- }
429
- else {
430
- // center
431
- offsetY = Math.floor(deltaHeight / 2);
432
- }
433
- return { offsetX, offsetY };
434
- };
435
- // First, handle the background layer using the helper method
436
- // Note: create_background_layer called inside the helper might destroy the old one
437
- this.background_layer = this._resize_background_layer(oldBackgroundLayer, // Pass the potentially null old layer
438
- newWidth, newHeight, scale, calculateOffset);
439
- // Now process regular layers
440
- const processedLayers = [];
441
- // Store old layer data before clearing the main arrays
442
- const oldLayerData = this.layers.map((oldLayer) => ({
443
- id: oldLayer.id,
444
- name: oldLayer.name,
445
- user_created: oldLayer.user_created,
446
- texture: this.draw_textures.get(oldLayer.container),
447
- container: oldLayer.container
448
- }));
449
- // Clear the current layers array and textures map. New layers will be added by the helper.
450
- this.layers = [];
451
- this.draw_textures.clear(); // Ensure map is cleared before helpers add to it
452
- // Rebuild layers in the original order using the helper method
453
- for (const oldData of oldLayerData) {
454
- // The helper _resize_single_layer handles creating the new layer,
455
- // drawing content, cleanup, and adding the new layer to this.layers.
456
- // It returns null if resizing failed for that layer.
457
- const newLayer = this._resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset);
458
- if (newLayer) {
459
- processedLayers.push(newLayer);
460
- }
461
- }
462
- // Ensure the active layer still exists, otherwise select the top one
463
- const currentActiveId = get(this.layer_store).active_layer;
464
- const activeLayerExists = processedLayers.some((l) => l.id === currentActiveId);
465
- if (!activeLayerExists && processedLayers.length > 0) {
466
- this.set_active_layer(processedLayers[0].id); // Set to top layer (index 0 is bottom visually)
467
- }
468
- else if (processedLayers.length === 0) {
469
- this.layer_store.update((s) => ({ ...s, active_layer: "" })); // No layers left
470
- }
471
- else {
472
- // If the active layer still exists, just trigger an update to ensure consistency
473
- this.layer_store.update((s) => ({ ...s }));
474
- }
475
- // Update Z-indices
476
- this.update_layer_order();
477
- // Final cleanup attempt - textures might need explicit destroy calls
478
- // Using setTimeout allows the current execution stack to clear
479
- setTimeout(() => {
480
- // Attempt to force GC or resource cleanup if needed, though Pixi often handles this
481
- Assets.cache.reset(); // Be careful with this, might clear too much
482
- this.app.renderer.textureGC.run(); // Force texture garbage collection
483
- }, 100);
484
- }
485
- /**
486
- * Helper method to resize the background layer.
487
- */
488
- _resize_background_layer(oldBackgroundLayer, newWidth, newHeight, scale, calculateOffset) {
489
- if (!oldBackgroundLayer) {
490
- // If no old background layer existed, create a new one
491
- return this.create_background_layer(newWidth, newHeight);
492
- }
493
- // Identify the actual image sprite within the background container
494
- let backgroundImage = oldBackgroundLayer.children.find((child) => child instanceof Sprite &&
495
- child.texture !== oldBackgroundLayer.children[0]?.texture);
496
- // Create the new background layer first (this also destroys the old one implicitly if it exists)
497
- const newBackgroundLayer = this.create_background_layer(newWidth, newHeight);
498
- if (backgroundImage) {
499
- const newBgImage = new Sprite(backgroundImage.texture);
500
- newBgImage.width = backgroundImage.width; // Preserve original size unless scaling
501
- newBgImage.height = backgroundImage.height;
502
- if (scale) {
503
- newBgImage.width = newWidth;
504
- newBgImage.height = newHeight;
505
- newBgImage.position.set(0, 0);
506
- }
507
- else {
508
- const { offsetX, offsetY } = calculateOffset();
509
- newBgImage.position.set(backgroundImage.x + offsetX, backgroundImage.y + offsetY);
510
- }
511
- newBackgroundLayer.addChild(newBgImage);
512
- }
513
- // If no background image, the new layer created above is already blank and correctly sized.
514
- return newBackgroundLayer;
515
- }
516
- /**
517
- * Helper method to resize a single regular layer.
518
- */
519
- _resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset) {
520
- if (!oldData.texture) {
521
- console.warn(`No texture found for layer ${oldData.id}, skipping cleanup.`);
522
- // Destroy the old container if it exists and is not destroyed
523
- if (oldData.container && !oldData.container.destroyed) {
524
- if (this.image_container.children.includes(oldData.container)) {
525
- this.image_container.removeChild(oldData.container);
526
- }
527
- oldData.container.destroy({ children: true });
528
- }
529
- return null;
530
- }
531
- // Create a new layer container and associated render texture
532
- const newContainer = this.create_layer({
533
- // This adds the layer to this.layers and draw_textures
534
- width: newWidth,
535
- height: newHeight,
536
- layer_name: oldData.name,
537
- user_created: oldData.user_created
538
- });
539
- // Retrieve the newly created layer data (it's the last one added by create_layer)
540
- const newLayer = this.layers[this.layers.length - 1];
541
- newLayer.id = oldData.id; // Restore the original ID
542
- // Get the render texture for the new layer
543
- const newTexture = this.draw_textures.get(newContainer);
544
- if (!newTexture) {
545
- console.error(`Failed to get texture for newly created layer ${newLayer.id}. Cleaning up.`);
546
- // Clean up the partially created new layer resources
547
- if (newContainer && !newContainer.destroyed) {
548
- if (this.image_container.children.includes(newContainer)) {
549
- this.image_container.removeChild(newContainer);
550
- }
551
- newContainer.destroy({ children: true });
552
- const idx = this.layers.findIndex((l) => l.container === newContainer);
553
- if (idx > -1)
554
- this.layers.splice(idx, 1);
555
- this.draw_textures.delete(newContainer);
556
- }
557
- // Clean up old resources
558
- if (oldData.texture && !oldData.texture.destroyed)
559
- oldData.texture.destroy(true);
560
- if (oldData.container && !oldData.container.destroyed)
561
- oldData.container.destroy({ children: true });
562
- return null;
563
- }
564
- // Explicitly clear the new render texture (create_layer might already do this, but explicit is safer)
565
- this.app.renderer.clear({ target: newTexture, clearColor: [0, 0, 0, 0] });
566
- // Create a sprite with the old texture content
567
- const sprite = new Sprite(oldData.texture);
568
- if (scale) {
569
- sprite.width = newWidth;
570
- sprite.height = newHeight;
571
- sprite.position.set(0, 0);
572
- }
573
- else {
574
- const { offsetX, offsetY } = calculateOffset();
575
- sprite.position.set(offsetX, offsetY);
576
- }
577
- // Render the sprite onto the new texture
578
- this.app.renderer.render(sprite, { renderTexture: newTexture });
579
- // Clean up temporary sprite and old resources
580
- sprite.destroy();
581
- if (oldData.texture && !oldData.texture.destroyed) {
582
- oldData.texture.destroy(true);
583
- }
584
- if (oldData.container && !oldData.container.destroyed) {
585
- if (this.image_container.children.includes(oldData.container)) {
586
- this.image_container.removeChild(oldData.container);
587
- }
588
- oldData.container.destroy({ children: true });
589
- }
590
- // Return the data for the successfully resized layer
591
- return newLayer;
592
- }
593
- async get_blobs(width, height) {
594
- const blobs = {
595
- background: await get_canvas_blob(this.app.renderer, this.background_layer, {
596
- width,
597
- height,
598
- x: 0,
599
- y: 0
600
- }),
601
- layers: await Promise.all(this.layers.map(async (layer) => {
602
- const blob = await get_canvas_blob(this.app.renderer, layer.container, {
603
- width,
604
- height,
605
- x: 0,
606
- y: 0
607
- });
608
- if (blob) {
609
- return blob;
610
- }
611
- return null;
612
- })),
613
- composite: await get_canvas_blob(this.app.renderer, this.image_container)
614
- };
615
- return blobs;
616
- }
617
- reset_layers(width, height, persist = false) {
618
- const _layers_to_recreate = persist
619
- ? this.layers.map((layer) => [layer.name, layer.id])
620
- : this.layer_options.layers.map((layer) => [layer, undefined]);
621
- this.layers.forEach((layer) => {
622
- this.delete_layer(layer.id);
623
- });
624
- for (const [layer_name, layer_id] of _layers_to_recreate) {
625
- this.create_layer({
626
- width,
627
- height,
628
- layer_name: layer_name,
629
- user_created: this.layer_options.layers.find((l) => l === layer_name)
630
- ? false
631
- : true,
632
- layer_id: layer_id
633
- });
634
- }
635
- if (!persist) {
636
- this.active_layer = this.layers[0].container;
637
- this.active_layer_id = this.layers[0].id;
638
- }
639
- else {
640
- // the id is the same but the layer is new, we need to find the new layer
641
- this.active_layer =
642
- this.layers.find((l) => l.id === this.active_layer_id)?.container ||
643
- this.layers[0].container;
644
- }
645
- this.layer_store.update((state) => ({
646
- active_layer: this.active_layer_id || this.layers[0].id,
647
- layers: this.layers
648
- }));
649
- }
650
- init_layers(width, height) {
651
- for (const layer of this.layers) {
652
- this.delete_layer(layer.id);
653
- }
654
- for (const layer of this.layer_options.layers) {
655
- this.create_layer({
656
- width,
657
- height,
658
- layer_name: layer,
659
- user_created: false
660
- });
661
- }
662
- this.active_layer = this.layers[0].container;
663
- this.active_layer_id = this.layers[0].id;
664
- this.layer_store.update((_layers) => ({
665
- active_layer: this.layers[0].id,
666
- layers: this.layers
667
- }));
668
- }
669
- }
8
+ import { CommandManager } from "./commands";
9
+ import { LayerManager, AddLayerCommand, RemoveLayerCommand, ReorderLayerCommand } from "./layers";
670
10
  const core_tools = ["image", "zoom"];
671
11
  const core_tool_map = {
672
12
  image: () => new ImageTool(),
@@ -685,7 +25,6 @@ class EditorState {
685
25
  if (!(editor instanceof ImageEditor)) {
686
26
  throw new Error("EditorState must be created by ImageEditor");
687
27
  }
688
- // Store private data
689
28
  this.scale = 1.0;
690
29
  this.position = { x: 0, y: 0 };
691
30
  this.subscribers = new Set();
@@ -706,7 +45,6 @@ class EditorState {
706
45
  subscribe: this.subscribe.bind(this)
707
46
  });
708
47
  }
709
- // Internal methods - only accessible to EditorState class
710
48
  _set_position(x, y) {
711
49
  const oldPosition = { ...this.position };
712
50
  this.position = { x, y };
@@ -727,7 +65,6 @@ class EditorState {
727
65
  });
728
66
  });
729
67
  }
730
- // Public method but only affects notifications
731
68
  subscribe(callback) {
732
69
  this.subscribers.add(callback);
733
70
  return () => this.subscribers.delete(callback);
@@ -858,13 +195,10 @@ export class ImageEditor {
858
195
  antialias: true,
859
196
  powerPreference: "high-performance"
860
197
  });
861
- // Set canvas properties
862
198
  const canvas = this.app.canvas;
863
199
  canvas.style.background = "transparent";
864
200
  await this.setup_containers();
865
- //create background layer
866
201
  this.layer_manager.create_background_layer(this.width, this.height);
867
- // Create initial layer
868
202
  this.layer_manager.init_layers(this.width, this.height);
869
203
  for (const tool of this.tools.values()) {
870
204
  await tool.setup(this.context, this.current_tool, this.current_subtool);
@@ -876,7 +210,6 @@ export class ImageEditor {
876
210
  });
877
211
  }
878
212
  this.target_element.appendChild(canvas);
879
- // Ensure parent element is transparent
880
213
  this.target_element.style.background = "transparent";
881
214
  this.dimensions.subscribe((dimensions) => {
882
215
  this.dimensions_value = dimensions;
@@ -894,10 +227,8 @@ export class ImageEditor {
894
227
  this.image_container.position.set(this.position_value.x, this.position_value.y);
895
228
  const effective_width = this.dimensions_value.width * this.scale_value;
896
229
  const effective_height = this.dimensions_value.height * this.scale_value;
897
- // Convert image_container's position (in stage space) to outline_container local coordinates.
898
230
  const local_x = Math.round(this.image_container.position.x - this.outline_container.position.x);
899
231
  const local_y = Math.round(this.image_container.position.y - this.outline_container.position.y);
900
- // Make sure overlay container follows the same position as outline container
901
232
  this.overlay_container.position.set(this.outline_container.position.x, this.outline_container.position.y);
902
233
  this.outline_graphics.clear();
903
234
  this.outline_graphics
@@ -906,16 +237,13 @@ export class ImageEditor {
906
237
  color: this.dark ? 0x333333 : 0xffffff,
907
238
  alpha: 1
908
239
  });
909
- // Draw border region indicator if border_region > 0
910
240
  if (this.border_region > 0) {
911
241
  const scaled_border = this.border_region * this.scale_value;
912
242
  const border_x = local_x + scaled_border - 1;
913
243
  const border_y = local_y + scaled_border - 1;
914
244
  const border_width = effective_width - scaled_border * 2 + 1;
915
245
  const border_height = effective_height - scaled_border * 2 + 1;
916
- // Clear previous overlay graphics
917
246
  this.overlay_graphics.clear();
918
- // Draw border rectangle using pixelLine for crisp 1px outline
919
247
  this.overlay_graphics
920
248
  .rect(border_x, border_y, border_width, border_height)
921
249
  .stroke({
@@ -924,36 +252,28 @@ export class ImageEditor {
924
252
  alpha: 0,
925
253
  pixelLine: true
926
254
  });
927
- // Create dashed line effect by drawing small rects along the border
928
255
  const dashLength = 5;
929
256
  const gapLength = 5;
930
257
  const totalLength = dashLength + gapLength;
931
258
  const lineColor = 0x999999;
932
- // Draw dashed horizontal lines (top and bottom)
933
259
  for (let x = border_x; x < border_x + border_width; x += totalLength) {
934
- // Top dash
935
260
  this.overlay_graphics
936
261
  .rect(x, border_y, Math.min(dashLength, border_x + border_width - x), 1)
937
262
  .fill({ color: lineColor, alpha: 0.7 });
938
- // Bottom dash
939
263
  this.overlay_graphics
940
264
  .rect(x, border_y + border_height, Math.min(dashLength, border_x + border_width - x), 1)
941
265
  .fill({ color: lineColor, alpha: 0.7 });
942
266
  }
943
- // Draw dashed vertical lines (left and right)
944
267
  for (let y = border_y; y < border_y + border_height; y += totalLength) {
945
- // Left dash
946
268
  this.overlay_graphics
947
269
  .rect(border_x, y, 1, Math.min(dashLength, border_y + border_height - y))
948
270
  .fill({ color: lineColor, alpha: 0.7 });
949
- // Right dash
950
271
  this.overlay_graphics
951
272
  .rect(border_x + border_width, y, 1, Math.min(dashLength, border_y + border_height - y))
952
273
  .fill({ color: lineColor, alpha: 0.7 });
953
274
  }
954
275
  }
955
276
  else {
956
- // Clear overlay graphics if no border region
957
277
  this.overlay_graphics.clear();
958
278
  }
959
279
  });
@@ -1020,7 +340,6 @@ export class ImageEditor {
1020
340
  if (this.app.renderer) {
1021
341
  this.app.renderer.resize(width, height);
1022
342
  }
1023
- // Instead, update the container position to stay centered
1024
343
  const app_center_x = width / 2;
1025
344
  const app_center_y = height / 2;
1026
345
  if (this.image_container) {
@@ -1036,6 +355,10 @@ export class ImageEditor {
1036
355
  zoom.cleanup();
1037
356
  zoom.setup(this.context, this.current_tool, this.current_subtool);
1038
357
  }
358
+ const brush = this.tools.get("brush");
359
+ if (brush) {
360
+ this.set_tool("draw");
361
+ }
1039
362
  }
1040
363
  async set_image_properties(properties) {
1041
364
  let hard = typeof properties.animate !== "undefined" ? !properties.animate : true;
@@ -1084,6 +407,7 @@ export class ImageEditor {
1084
407
  * @param url The URL of the image to add
1085
408
  */
1086
409
  async add_image_from_url(url) {
410
+ this.command_manager.reset();
1087
411
  const image_tool = this.tools.get("image");
1088
412
  const texture = await Assets.load(url);
1089
413
  await image_tool.add_image({
@@ -1091,13 +415,10 @@ export class ImageEditor {
1091
415
  fixed_canvas: this.fixed_canvas,
1092
416
  border_region: this.border_region
1093
417
  });
1094
- // Update resize tool if present
1095
418
  const resize_tool = this.tools.get("resize");
1096
419
  if (resize_tool && typeof resize_tool.set_border_region === "function") {
1097
420
  resize_tool.set_border_region(this.border_region);
1098
421
  }
1099
- this.notify("change");
1100
- this.notify("input");
1101
422
  }
1102
423
  set_tool(tool) {
1103
424
  this.current_tool = tool;
@@ -1116,10 +437,8 @@ export class ImageEditor {
1116
437
  }
1117
438
  async reset_canvas() {
1118
439
  this.layer_manager.reset_layers(this.width, this.height);
1119
- // Clear background image
1120
440
  this.background_image = undefined;
1121
441
  this.background_image_present.set(false);
1122
- // Reset position, scale and dimensions to default values
1123
442
  await this.set_image_properties({
1124
443
  width: this.width,
1125
444
  height: this.height,
@@ -1127,11 +446,8 @@ export class ImageEditor {
1127
446
  position: { x: 0, y: 0 },
1128
447
  animate: false
1129
448
  });
1130
- // Reset command stacks
1131
449
  this.command_manager = new CommandManager();
1132
- //create background layer
1133
450
  this.layer_manager.create_background_layer(this.width, this.height);
1134
- // Reset tools
1135
451
  for (const tool of this.tools.values()) {
1136
452
  tool.cleanup();
1137
453
  tool.setup(this.context, this.current_tool, this.current_subtool);
@@ -1145,14 +461,13 @@ export class ImageEditor {
1145
461
  this.notify("change");
1146
462
  }
1147
463
  add_layer() {
1148
- this.layer_manager.create_layer({
464
+ const add_layer_command = new AddLayerCommand(this.context, {
1149
465
  width: this.width,
1150
466
  height: this.height,
1151
- layer_name: undefined,
1152
467
  user_created: true,
1153
- layer_id: undefined,
1154
468
  make_active: true
1155
469
  });
470
+ this.command_manager.execute(add_layer_command);
1156
471
  this.notify("change");
1157
472
  }
1158
473
  /**
@@ -1161,6 +476,7 @@ export class ImageEditor {
1161
476
  * @returns A Promise that resolves when all layers are added
1162
477
  */
1163
478
  async add_layers_from_url(layer_urls) {
479
+ this.command_manager.reset();
1164
480
  const _layers = this.layer_manager.get_layers();
1165
481
  _layers.forEach((l) => this.layer_manager.delete_layer(l.id));
1166
482
  if (layer_urls === undefined || layer_urls.length === 0) {
@@ -1176,7 +492,6 @@ export class ImageEditor {
1176
492
  await this.layer_manager.add_layer_from_url(url);
1177
493
  }
1178
494
  this.layer_manager.set_active_layer(_layers[0].id);
1179
- // Update resize tool if present with border region
1180
495
  const resize_tool = this.tools.get("resize");
1181
496
  if (resize_tool && typeof resize_tool.set_border_region === "function") {
1182
497
  resize_tool.set_border_region(this.border_region);
@@ -1189,28 +504,26 @@ export class ImageEditor {
1189
504
  this.notify("change");
1190
505
  }
1191
506
  move_layer(id, direction) {
1192
- this.layer_manager.move_layer(id, direction);
507
+ const reorder_layer_command = new ReorderLayerCommand(this.context, id, direction);
508
+ this.command_manager.execute(reorder_layer_command);
1193
509
  this.notify("change");
1194
510
  }
1195
511
  delete_layer(id) {
1196
- this.layer_manager.delete_layer(id);
512
+ const remove_layer_command = new RemoveLayerCommand(this.context, id);
513
+ this.command_manager.execute(remove_layer_command);
1197
514
  this.notify("change");
1198
515
  }
1199
516
  modify_canvas_size(width, height, anchor, scale) {
1200
- // Store old dimensions before updating
1201
517
  const oldWidth = this.width;
1202
518
  const oldHeight = this.height;
1203
- // Resize layers first, passing old dimensions for anchor calculation
1204
519
  this.layer_manager.resize_all_layers(width, height, scale, anchor, oldWidth, oldHeight);
1205
- // Update the editor's main dimension tracking *after* resizing layers
1206
520
  this.width = width;
1207
521
  this.height = height;
1208
- // Update the dimensions store and reset view scale/position
1209
522
  this.set_image_properties({
1210
523
  width,
1211
524
  height,
1212
- scale: 1, // Reset scale
1213
- position: { x: 0, y: 0 }, // Reset pan/position of the image_container
525
+ scale: 1,
526
+ position: { x: 0, y: 0 },
1214
527
  animate: false
1215
528
  });
1216
529
  this.notify("change");
@@ -1236,9 +549,6 @@ export class ImageEditor {
1236
549
  destroy() {
1237
550
  if (!this.app)
1238
551
  return;
1239
- // for (const tool of this.tools) {
1240
- // tool[1]?.cleanup();
1241
- // }
1242
552
  this.app?.destroy();
1243
553
  }
1244
554
  resize(width, height) {