@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
@@ -0,0 +1,1068 @@
1
+ import {
2
+ Application,
3
+ Container,
4
+ Graphics,
5
+ Sprite,
6
+ RenderTexture,
7
+ SCALE_MODES,
8
+ Texture,
9
+ Assets
10
+ } from "pixi.js";
11
+
12
+ import { get_canvas_blob } from "../utils/pixi";
13
+ import type { Writable } from "svelte/store";
14
+ import { writable, get } from "svelte/store";
15
+ import { type ImageBlobs, type LayerOptions } from "../types";
16
+ import { type Command } from "./commands";
17
+ import { type ImageEditorContext } from "./editor";
18
+
19
+ export class LayerManager {
20
+ private layers: {
21
+ name: string;
22
+ id: string;
23
+ container: Container;
24
+ user_created: boolean;
25
+ visible: boolean;
26
+ }[] = [];
27
+ private active_layer: Container | null = null;
28
+ private active_layer_id: string | null = null;
29
+ private draw_textures: Map<Container, RenderTexture> = new Map();
30
+ layer_store: Writable<{
31
+ active_layer: string;
32
+ layers: {
33
+ name: string;
34
+ id: string;
35
+ user_created: boolean;
36
+ visible: boolean;
37
+ }[];
38
+ }> = writable({
39
+ active_layer: "",
40
+
41
+ layers: []
42
+ });
43
+ private background_layer: Container | null = null;
44
+ private image_container: Container;
45
+ private app: Application;
46
+ private fixed_canvas: boolean;
47
+ private dark: boolean;
48
+ private border_region: number;
49
+ private layer_options: LayerOptions;
50
+ constructor(
51
+ image_container: Container,
52
+ app: Application,
53
+ fixed_canvas: boolean,
54
+ dark: boolean,
55
+ border_region: number,
56
+ layer_options: LayerOptions
57
+ ) {
58
+ this.image_container = image_container;
59
+ this.app = app;
60
+ this.fixed_canvas = fixed_canvas;
61
+ this.dark = dark;
62
+ this.border_region = border_region;
63
+ this.layer_options = layer_options;
64
+ }
65
+
66
+ toggle_layer_visibility(id: string): void {
67
+ const layer = this.layers.find((l) => l.id === id);
68
+ if (layer) {
69
+ layer.container.visible = !layer.container.visible;
70
+ layer.visible = layer.container.visible;
71
+ this.layer_store.update((state) => ({
72
+ active_layer: state.active_layer,
73
+ layers: this.layers
74
+ }));
75
+ }
76
+ }
77
+
78
+ create_background_layer(width: number, height: number): Container {
79
+ if (this.background_layer) {
80
+ this.background_layer.destroy();
81
+ }
82
+
83
+ const layer = new Container();
84
+
85
+ this.background_layer = layer;
86
+
87
+ const bg_texture = RenderTexture.create({
88
+ width,
89
+ height,
90
+ resolution: window.devicePixelRatio,
91
+ antialias: true,
92
+ scaleMode: SCALE_MODES.NEAREST
93
+ });
94
+
95
+ const bg_sprite = new Sprite(bg_texture);
96
+ layer.addChild(bg_sprite);
97
+
98
+ const clear_graphics = new Graphics();
99
+ clear_graphics.clear();
100
+
101
+ clear_graphics
102
+ .rect(0, 0, width, height)
103
+ .fill({ color: this.dark ? 0x333333 : 0xffffff, alpha: 1 });
104
+
105
+ this.app.renderer.render({
106
+ container: clear_graphics,
107
+ target: bg_texture,
108
+ clear: true
109
+ });
110
+
111
+ this.image_container.addChild(layer);
112
+
113
+ layer.zIndex = -1;
114
+
115
+ this.update_layer_order();
116
+ return layer;
117
+ }
118
+
119
+ set_layer_options(
120
+ layer_options: LayerOptions,
121
+ width: number,
122
+ height: number
123
+ ): void {
124
+ this.layer_options = layer_options;
125
+ this.reset_layers(width, height);
126
+ }
127
+
128
+ /**
129
+ * Creates a background layer from an image URL
130
+ * @param url The URL of the image to use as background
131
+ * @param width The width of the layer (if fixed_canvas is true)
132
+ * @param height The height of the layer (if fixed_canvas is true)
133
+ * @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
134
+ * @returns A Promise that resolves to the background Container when the image is loaded
135
+ */
136
+ async create_background_layer_from_url(
137
+ url: string,
138
+ width?: number,
139
+ height?: number
140
+ ): Promise<Container> {
141
+ const layer = this.create_background_layer(
142
+ width || this.image_container.width,
143
+ height || this.image_container.height
144
+ );
145
+
146
+ try {
147
+ const texture = await Texture.from(url);
148
+ const sprite = new Sprite(texture);
149
+
150
+ const imageWidth = sprite.texture.width;
151
+ const imageHeight = sprite.texture.height;
152
+
153
+ const containerWidth = width || this.image_container.width;
154
+ const containerHeight = height || this.image_container.height;
155
+
156
+ if (this.fixed_canvas) {
157
+ const effectiveContainerWidth = Math.max(
158
+ containerWidth - this.border_region * 2,
159
+ 10
160
+ );
161
+ const effectiveContainerHeight = Math.max(
162
+ containerHeight - this.border_region * 2,
163
+ 10
164
+ );
165
+
166
+ const imageAspectRatio = imageWidth / imageHeight;
167
+ const containerAspectRatio =
168
+ effectiveContainerWidth / effectiveContainerHeight;
169
+
170
+ let finalWidth, finalHeight;
171
+ let posX = this.border_region,
172
+ posY = this.border_region;
173
+
174
+ if (
175
+ imageWidth <= effectiveContainerWidth &&
176
+ imageHeight <= effectiveContainerHeight
177
+ ) {
178
+ finalWidth = imageWidth;
179
+ finalHeight = imageHeight;
180
+ } else {
181
+ if (imageAspectRatio > containerAspectRatio) {
182
+ finalWidth = effectiveContainerWidth;
183
+ finalHeight = effectiveContainerWidth / imageAspectRatio;
184
+ } else {
185
+ finalHeight = effectiveContainerHeight;
186
+ finalWidth = effectiveContainerHeight * imageAspectRatio;
187
+ }
188
+ }
189
+
190
+ posX += Math.round((effectiveContainerWidth - finalWidth) / 2);
191
+ posY += Math.round((effectiveContainerHeight - finalHeight) / 2);
192
+ sprite.width = finalWidth;
193
+ sprite.height = finalHeight;
194
+ sprite.position.set(posX, posY);
195
+ } else {
196
+ sprite.position.set(this.border_region, this.border_region);
197
+
198
+ if (this.background_layer) {
199
+ this.background_layer.destroy();
200
+ }
201
+
202
+ const totalWidth = imageWidth + this.border_region * 2;
203
+ const totalHeight = imageHeight + this.border_region * 2;
204
+ const newLayer = this.create_background_layer(totalWidth, totalHeight);
205
+
206
+ sprite.width = imageWidth;
207
+ sprite.height = imageHeight;
208
+ newLayer.addChild(sprite);
209
+
210
+ return newLayer;
211
+ }
212
+
213
+ layer.addChild(sprite);
214
+
215
+ return layer;
216
+ } catch (error) {
217
+ console.error("Error loading image from URL:", error);
218
+ return layer;
219
+ }
220
+ }
221
+
222
+ create_layer({
223
+ width,
224
+ height,
225
+ layer_name,
226
+ user_created,
227
+ layer_id = undefined,
228
+ make_active = false
229
+ }: {
230
+ width: number;
231
+ height: number;
232
+ layer_name?: string;
233
+ user_created: boolean;
234
+ layer_id?: string;
235
+ make_active?: boolean;
236
+ }): Container {
237
+ const layer = new Container();
238
+ const _layer_id = layer_id || Math.random().toString(36).substring(2, 15);
239
+ const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
240
+
241
+ this.layers.push({
242
+ name: _layer_name,
243
+ id: _layer_id,
244
+ container: layer,
245
+ user_created,
246
+ visible: true
247
+ });
248
+
249
+ this.image_container.addChild(layer);
250
+
251
+ const draw_texture = RenderTexture.create({
252
+ width,
253
+ height,
254
+ resolution: window.devicePixelRatio,
255
+ antialias: true,
256
+ scaleMode: SCALE_MODES.NEAREST
257
+ });
258
+
259
+ const canvas_sprite = new Sprite(draw_texture);
260
+ layer.addChild(canvas_sprite);
261
+
262
+ const clear_graphics = new Graphics();
263
+ clear_graphics.clear();
264
+ clear_graphics.beginFill(0, 0);
265
+ clear_graphics.drawRect(0, 0, width, height);
266
+ clear_graphics.endFill();
267
+
268
+ this.app.renderer.render({
269
+ container: clear_graphics,
270
+ target: draw_texture,
271
+ clear: true
272
+ });
273
+
274
+ this.draw_textures.set(layer, draw_texture);
275
+
276
+ this.update_layer_order();
277
+ if (make_active) {
278
+ this.set_active_layer(_layer_id);
279
+ }
280
+
281
+ this.layer_store.set({
282
+ active_layer: this.active_layer_id || "",
283
+ layers: this.layers
284
+ });
285
+ return layer;
286
+ }
287
+
288
+ /**
289
+ * Creates a new layer with an image loaded from a URL
290
+ * @param url The URL of the image to load
291
+ * @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
292
+ * @returns A Promise that resolves to the layer ID
293
+ */
294
+ async add_layer_from_url(url: string): Promise<string> {
295
+ const { width, height } = this.image_container.getLocalBounds();
296
+ const layer = this.create_layer({
297
+ width,
298
+ height,
299
+ layer_name: "Layer 1",
300
+ user_created: true
301
+ });
302
+
303
+ const layerIndex = this.layers.findIndex((l) => l.container === layer);
304
+ if (layerIndex === -1) {
305
+ console.error("Could not find newly created layer");
306
+ return "";
307
+ }
308
+ const layerId = this.layers[layerIndex].id;
309
+
310
+ try {
311
+ const texture = await Assets.load(url);
312
+
313
+ const drawTexture = this.draw_textures.get(layer);
314
+ if (!drawTexture) {
315
+ console.error("No draw texture found for layer");
316
+ return layerId;
317
+ }
318
+
319
+ const sprite = new Sprite(texture);
320
+ const imageWidth = sprite.width;
321
+ const imageHeight = sprite.height;
322
+
323
+ let posX = this.border_region;
324
+ let posY = this.border_region;
325
+
326
+ const effectiveWidth = this.fixed_canvas
327
+ ? width - this.border_region * 2
328
+ : width;
329
+ const effectiveHeight = this.fixed_canvas
330
+ ? height - this.border_region * 2
331
+ : height;
332
+
333
+ if (imageWidth < effectiveWidth || imageHeight < effectiveHeight) {
334
+ posX = Math.floor((effectiveWidth - imageWidth) / 2);
335
+ posY = Math.floor((effectiveHeight - imageHeight) / 2);
336
+ }
337
+
338
+ sprite.position.set(posX, posY);
339
+
340
+ if (imageWidth > effectiveWidth || imageHeight > effectiveHeight) {
341
+ const imageAspectRatio = imageWidth / imageHeight;
342
+ const areaAspectRatio = effectiveWidth / effectiveHeight;
343
+
344
+ let finalWidth, finalHeight;
345
+
346
+ if (imageAspectRatio > areaAspectRatio) {
347
+ finalWidth = effectiveWidth;
348
+ finalHeight = effectiveWidth / imageAspectRatio;
349
+ } else {
350
+ finalHeight = effectiveHeight;
351
+ finalWidth = effectiveHeight * imageAspectRatio;
352
+ }
353
+
354
+ sprite.width = finalWidth;
355
+ sprite.height = finalHeight;
356
+
357
+ posX =
358
+ this.border_region + Math.floor((effectiveWidth - finalWidth) / 2);
359
+ posY =
360
+ this.border_region + Math.floor((effectiveHeight - finalHeight) / 2);
361
+ sprite.position.set(posX, posY);
362
+ }
363
+
364
+ this.app.renderer.render(sprite, { renderTexture: drawTexture });
365
+
366
+ this.set_active_layer(layerId);
367
+
368
+ return layerId;
369
+ } catch (error) {
370
+ console.error("Error loading image from URL:", error);
371
+ return layerId;
372
+ }
373
+ }
374
+
375
+ get_active_layer(): typeof this.active_layer {
376
+ return this.active_layer;
377
+ }
378
+
379
+ set_active_layer(id: string): void {
380
+ if (this.layers.some((l) => l.id === id)) {
381
+ this.active_layer =
382
+ this.layers.find((l) => l.id === id)?.container ||
383
+ this.layers[0]?.container ||
384
+ null;
385
+ this.active_layer_id = id;
386
+ this.layer_store.set({
387
+ active_layer: id,
388
+ layers: this.layers
389
+ });
390
+ }
391
+ }
392
+
393
+ get_layers(): typeof this.layers {
394
+ return this.layers;
395
+ }
396
+
397
+ get_layer_textures(id: string): { draw: RenderTexture } | null {
398
+ const layer = this.layers.find((l) => l.id === id);
399
+ if (layer) {
400
+ const draw = this.draw_textures.get(layer.container);
401
+ if (draw) {
402
+ return { draw };
403
+ }
404
+ }
405
+ return null;
406
+ }
407
+
408
+ delete_layer(id: string): void {
409
+ const index = this.layers.findIndex((l) => l.id === id);
410
+ if (index > -1) {
411
+ const draw_texture = this.draw_textures.get(this.layers[index].container);
412
+ if (draw_texture) {
413
+ draw_texture.destroy();
414
+ this.draw_textures.delete(this.layers[index].container);
415
+ }
416
+ this.layers[index].container.destroy();
417
+ if (this.active_layer === this.layers[index].container) {
418
+ const new_active_layer = this.layers[Math.max(0, index - 1)] || null;
419
+ this.active_layer = new_active_layer?.container || null;
420
+ this.active_layer_id = new_active_layer?.id || null;
421
+ }
422
+ this.layers = this.layers.filter((l) => l.id !== id);
423
+
424
+ this.layer_store.update((_layers) => ({
425
+ active_layer:
426
+ _layers.active_layer === id
427
+ ? this.layers[this.layers.length - 1]?.id
428
+ : _layers.active_layer,
429
+ layers: this.layers
430
+ }));
431
+
432
+ this.update_layer_order();
433
+ }
434
+ }
435
+
436
+ private update_layer_order(): void {
437
+ if (this.background_layer) {
438
+ this.background_layer.zIndex = -1;
439
+ }
440
+ this.layers.forEach((layer, index) => {
441
+ layer.container.zIndex = index;
442
+ });
443
+ }
444
+
445
+ move_layer(id: string, direction: "up" | "down"): void {
446
+ const index = this.layers.findIndex((l) => l.id === id);
447
+ if (index > -1) {
448
+ const new_index = direction === "up" ? index - 1 : index + 1;
449
+
450
+ this.layers = this.layers.map((l, i) => {
451
+ if (i === index) {
452
+ return this.layers[new_index];
453
+ }
454
+ if (i === new_index) {
455
+ return this.layers[index];
456
+ }
457
+ return l;
458
+ });
459
+ this.update_layer_order();
460
+ this.layer_store.update((_layers) => ({
461
+ active_layer: id,
462
+ layers: this.layers
463
+ }));
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Resizes all layers to a new width and height
469
+ * @param newWidth The new width of the layers
470
+ * @param newHeight The new height of the layers
471
+ * @param scale If true, scales the layer content to fit the new dimensions. If false, keeps the content size unchanged.
472
+ * @param anchor The anchor point to position the content relative to the new size
473
+ */
474
+ resize_all_layers(
475
+ newWidth: number,
476
+ newHeight: number,
477
+ scale: boolean,
478
+ anchor:
479
+ | "top-left"
480
+ | "top"
481
+ | "top-right"
482
+ | "left"
483
+ | "center"
484
+ | "right"
485
+ | "bottom-left"
486
+ | "bottom"
487
+ | "bottom-right",
488
+ oldCanvasWidth: number,
489
+ oldCanvasHeight: number
490
+ ): void {
491
+ const oldLayersById = new Map(this.layers.map((l) => [l.id, l]));
492
+ const oldBackgroundLayer = this.background_layer;
493
+
494
+ const calculateOffset = (): { offsetX: number; offsetY: number } => {
495
+ let offsetX = 0;
496
+ let offsetY = 0;
497
+ const deltaWidth = newWidth - oldCanvasWidth;
498
+ const deltaHeight = newHeight - oldCanvasHeight;
499
+
500
+ if (anchor.includes("left")) {
501
+ offsetX = 0;
502
+ } else if (anchor.includes("right")) {
503
+ offsetX = deltaWidth;
504
+ } else {
505
+ offsetX = Math.floor(deltaWidth / 2);
506
+ }
507
+
508
+ if (anchor.includes("top")) {
509
+ offsetY = 0;
510
+ } else if (anchor.includes("bottom")) {
511
+ offsetY = deltaHeight;
512
+ } else {
513
+ offsetY = Math.floor(deltaHeight / 2);
514
+ }
515
+
516
+ return { offsetX, offsetY };
517
+ };
518
+
519
+ this.background_layer = this._resize_background_layer(
520
+ oldBackgroundLayer,
521
+ newWidth,
522
+ newHeight,
523
+ scale,
524
+ calculateOffset
525
+ );
526
+
527
+ const processedLayers: {
528
+ name: string;
529
+ id: string;
530
+ container: Container;
531
+ user_created: boolean;
532
+ }[] = [];
533
+
534
+ const oldLayerData = this.layers.map((oldLayer) => ({
535
+ id: oldLayer.id,
536
+ name: oldLayer.name,
537
+ user_created: oldLayer.user_created,
538
+ texture: this.draw_textures.get(oldLayer.container),
539
+ container: oldLayer.container
540
+ }));
541
+
542
+ this.layers = [];
543
+ this.draw_textures.clear();
544
+ for (const oldData of oldLayerData) {
545
+ const newLayer = this._resize_single_layer(
546
+ oldData,
547
+ newWidth,
548
+ newHeight,
549
+ scale,
550
+ calculateOffset
551
+ );
552
+ if (newLayer) {
553
+ processedLayers.push(newLayer);
554
+ }
555
+ }
556
+
557
+ const currentActiveId = get(this.layer_store).active_layer;
558
+ const activeLayerExists = processedLayers.some(
559
+ (l) => l.id === currentActiveId
560
+ );
561
+
562
+ if (!activeLayerExists && processedLayers.length > 0) {
563
+ this.set_active_layer(processedLayers[0].id);
564
+ } else if (processedLayers.length === 0) {
565
+ this.layer_store.update((s) => ({ ...s, active_layer: "" }));
566
+ } else {
567
+ this.layer_store.update((s) => ({ ...s }));
568
+ }
569
+
570
+ this.update_layer_order();
571
+
572
+ setTimeout(() => {
573
+ Assets.cache.reset();
574
+ this.app.renderer.textureGC.run();
575
+ }, 100);
576
+ }
577
+
578
+ /**
579
+ * Helper method to resize the background layer.
580
+ */
581
+ private _resize_background_layer(
582
+ oldBackgroundLayer: Container | null,
583
+ newWidth: number,
584
+ newHeight: number,
585
+ scale: boolean,
586
+ calculateOffset: () => { offsetX: number; offsetY: number }
587
+ ): Container | null {
588
+ if (!oldBackgroundLayer) {
589
+ return this.create_background_layer(newWidth, newHeight);
590
+ }
591
+
592
+ let backgroundImage: Sprite | null = oldBackgroundLayer.children.find(
593
+ (child) =>
594
+ child instanceof Sprite &&
595
+ child.texture !== (oldBackgroundLayer.children[0] as Sprite)?.texture
596
+ ) as Sprite | null;
597
+
598
+ const newBackgroundLayer = this.create_background_layer(
599
+ newWidth,
600
+ newHeight
601
+ );
602
+
603
+ if (backgroundImage) {
604
+ const newBgImage = new Sprite(backgroundImage.texture);
605
+ newBgImage.width = backgroundImage.width;
606
+ newBgImage.height = backgroundImage.height;
607
+
608
+ if (scale) {
609
+ newBgImage.width = newWidth;
610
+ newBgImage.height = newHeight;
611
+ newBgImage.position.set(0, 0);
612
+ } else {
613
+ const { offsetX, offsetY } = calculateOffset();
614
+ newBgImage.position.set(
615
+ backgroundImage.x + offsetX,
616
+ backgroundImage.y + offsetY
617
+ );
618
+ }
619
+ newBackgroundLayer.addChild(newBgImage);
620
+ }
621
+
622
+ return newBackgroundLayer;
623
+ }
624
+
625
+ /**
626
+ * Helper method to resize a single regular layer.
627
+ */
628
+ private _resize_single_layer(
629
+ oldData: {
630
+ id: string;
631
+ name: string;
632
+ user_created: boolean;
633
+ texture: RenderTexture | undefined;
634
+ container: Container;
635
+ },
636
+ newWidth: number,
637
+ newHeight: number,
638
+ scale: boolean,
639
+ calculateOffset: () => { offsetX: number; offsetY: number }
640
+ ): {
641
+ name: string;
642
+ id: string;
643
+ container: Container;
644
+ user_created: boolean;
645
+ } | null {
646
+ if (!oldData.texture) {
647
+ console.warn(
648
+ `No texture found for layer ${oldData.id}, skipping cleanup.`
649
+ );
650
+ if (oldData.container && !oldData.container.destroyed) {
651
+ if (this.image_container.children.includes(oldData.container)) {
652
+ this.image_container.removeChild(oldData.container);
653
+ }
654
+ oldData.container.destroy({ children: true });
655
+ }
656
+ return null;
657
+ }
658
+
659
+ const newContainer = this.create_layer({
660
+ width: newWidth,
661
+ height: newHeight,
662
+ layer_name: oldData.name,
663
+ user_created: oldData.user_created
664
+ });
665
+
666
+ const newLayer = this.layers[this.layers.length - 1];
667
+ newLayer.id = oldData.id;
668
+ const newTexture = this.draw_textures.get(newContainer);
669
+
670
+ if (!newTexture) {
671
+ console.error(
672
+ `Failed to get texture for newly created layer ${newLayer.id}. Cleaning up.`
673
+ );
674
+ if (newContainer && !newContainer.destroyed) {
675
+ if (this.image_container.children.includes(newContainer)) {
676
+ this.image_container.removeChild(newContainer);
677
+ }
678
+ newContainer.destroy({ children: true });
679
+ const idx = this.layers.findIndex((l) => l.container === newContainer);
680
+ if (idx > -1) this.layers.splice(idx, 1);
681
+ this.draw_textures.delete(newContainer);
682
+ }
683
+ if (oldData.texture && !oldData.texture.destroyed)
684
+ oldData.texture.destroy(true);
685
+ if (oldData.container && !oldData.container.destroyed)
686
+ oldData.container.destroy({ children: true });
687
+ return null;
688
+ }
689
+
690
+ this.app.renderer.clear({ target: newTexture, clearColor: [0, 0, 0, 0] });
691
+
692
+ const sprite = new Sprite(oldData.texture);
693
+
694
+ if (scale) {
695
+ sprite.width = newWidth;
696
+ sprite.height = newHeight;
697
+ sprite.position.set(0, 0);
698
+ } else {
699
+ const { offsetX, offsetY } = calculateOffset();
700
+ sprite.position.set(offsetX, offsetY);
701
+ }
702
+
703
+ this.app.renderer.render(sprite, { renderTexture: newTexture });
704
+
705
+ sprite.destroy();
706
+ if (oldData.texture && !oldData.texture.destroyed) {
707
+ oldData.texture.destroy(true);
708
+ }
709
+ if (oldData.container && !oldData.container.destroyed) {
710
+ if (this.image_container.children.includes(oldData.container)) {
711
+ this.image_container.removeChild(oldData.container);
712
+ }
713
+ oldData.container.destroy({ children: true });
714
+ }
715
+
716
+ return newLayer;
717
+ }
718
+
719
+ async get_blobs(width: number, height: number): Promise<ImageBlobs> {
720
+ const blobs = {
721
+ background: await get_canvas_blob(
722
+ this.app.renderer,
723
+ this.background_layer,
724
+ {
725
+ width,
726
+ height,
727
+ x: 0,
728
+ y: 0
729
+ }
730
+ ),
731
+ layers: await Promise.all(
732
+ this.layers.map(async (layer) => {
733
+ const blob = await get_canvas_blob(
734
+ this.app.renderer,
735
+ layer.container,
736
+ {
737
+ width,
738
+ height,
739
+ x: 0,
740
+ y: 0
741
+ }
742
+ );
743
+ if (blob) {
744
+ return blob;
745
+ }
746
+ return null;
747
+ })
748
+ ),
749
+ composite: await get_canvas_blob(this.app.renderer, this.image_container)
750
+ };
751
+
752
+ return blobs;
753
+ }
754
+
755
+ reset_layers(width: number, height: number, persist = false): void {
756
+ const _layers_to_recreate = persist
757
+ ? this.layers.map((layer) => [layer.name, layer.id])
758
+ : this.layer_options.layers.map((layer) => [layer, undefined]);
759
+
760
+ this.layers.forEach((layer) => {
761
+ this.delete_layer(layer.id);
762
+ });
763
+
764
+ for (const [layer_name, layer_id] of _layers_to_recreate) {
765
+ this.create_layer({
766
+ width,
767
+ height,
768
+ layer_name: layer_name,
769
+ user_created: this.layer_options.layers.find((l) => l === layer_name)
770
+ ? false
771
+ : true,
772
+ layer_id: layer_id
773
+ });
774
+ }
775
+
776
+ if (!persist) {
777
+ this.active_layer = this.layers[0].container;
778
+ this.active_layer_id = this.layers[0].id;
779
+ } else {
780
+ this.active_layer =
781
+ this.layers.find((l) => l.id === this.active_layer_id)?.container ||
782
+ this.layers[0]?.container;
783
+
784
+ if (!this.active_layer) return;
785
+ }
786
+
787
+ this.layer_store.update((state) => ({
788
+ active_layer: this.active_layer_id || this.layers[0].id,
789
+ layers: this.layers
790
+ }));
791
+ }
792
+
793
+ init_layers(width: number, height: number): void {
794
+ for (const layer of this.layers) {
795
+ this.delete_layer(layer.id);
796
+ }
797
+ for (const layer of this.layer_options.layers) {
798
+ this.create_layer({
799
+ width,
800
+ height,
801
+ layer_name: layer,
802
+ user_created: false
803
+ });
804
+ }
805
+
806
+ this.active_layer = this.layers[0].container;
807
+ this.active_layer_id = this.layers[0].id;
808
+ this.layer_store.update((_layers) => ({
809
+ active_layer: this.layers[0].id,
810
+ layers: this.layers
811
+ }));
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Command to add a new layer
817
+ */
818
+ export class AddLayerCommand implements Command {
819
+ private layer_id: string;
820
+ private layer_name: string;
821
+ private width: number;
822
+ private height: number;
823
+ private user_created: boolean;
824
+ private make_active: boolean;
825
+ private previous_active_layer: string | null = null;
826
+ name: string;
827
+ constructor(
828
+ private context: ImageEditorContext,
829
+ options: {
830
+ width: number;
831
+ height: number;
832
+ layer_name?: string;
833
+ user_created: boolean;
834
+ layer_id?: string;
835
+ make_active?: boolean;
836
+ }
837
+ ) {
838
+ this.width = options.width;
839
+ this.height = options.height;
840
+ this.layer_name =
841
+ options.layer_name ||
842
+ `Layer ${this.context.layer_manager.get_layers().length + 1}`;
843
+ this.user_created = options.user_created;
844
+ this.layer_id =
845
+ options.layer_id || Math.random().toString(36).substring(2, 15);
846
+ this.make_active = options.make_active || false;
847
+ this.name = "AddLayer";
848
+ const current_layers = this.context.layer_manager.get_layers();
849
+ const current_active = current_layers.find(
850
+ (l) => l.container === this.context.layer_manager.get_active_layer()
851
+ );
852
+ this.previous_active_layer = current_active?.id || null;
853
+ }
854
+
855
+ async execute(): Promise<void> {
856
+ this.context.layer_manager.create_layer({
857
+ width: this.width,
858
+ height: this.height,
859
+ layer_name: this.layer_name,
860
+ user_created: this.user_created,
861
+ layer_id: this.layer_id,
862
+ make_active: this.make_active
863
+ });
864
+ }
865
+
866
+ async undo(): Promise<void> {
867
+ this.context.layer_manager.delete_layer(this.layer_id);
868
+
869
+ if (this.previous_active_layer) {
870
+ this.context.layer_manager.set_active_layer(this.previous_active_layer);
871
+ }
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Command to remove a layer
877
+ */
878
+ export class RemoveLayerCommand implements Command {
879
+ private layer_data: {
880
+ id: string;
881
+ name: string;
882
+ user_created: boolean;
883
+ visible: boolean;
884
+ was_active: boolean;
885
+ };
886
+ private previous_active_layer: string | null = null;
887
+ private texture_copy: RenderTexture | null = null;
888
+ name: string;
889
+
890
+ constructor(
891
+ private context: ImageEditorContext,
892
+ layer_id: string
893
+ ) {
894
+ this.name = "RemoveLayer";
895
+ const layers = this.context.layer_manager.get_layers();
896
+ const layer_to_remove = layers.find((l) => l.id === layer_id);
897
+
898
+ if (!layer_to_remove) {
899
+ throw new Error(`Layer with ID ${layer_id} not found`);
900
+ }
901
+
902
+ const active_layer = this.context.layer_manager.get_active_layer();
903
+ const was_active = layer_to_remove.container === active_layer;
904
+
905
+ if (was_active) {
906
+ const layer_index = layers.findIndex((l) => l.id === layer_id);
907
+ const next_active = layers[Math.max(0, layer_index - 1)];
908
+ this.previous_active_layer = next_active?.id || null;
909
+ }
910
+
911
+ this.layer_data = {
912
+ id: layer_to_remove.id,
913
+ name: layer_to_remove.name,
914
+ user_created: layer_to_remove.user_created,
915
+ visible: layer_to_remove.visible,
916
+ was_active
917
+ };
918
+
919
+ this.captureTextureData(layer_id);
920
+ }
921
+
922
+ /**
923
+ * Create a direct copy of the layer's texture
924
+ */
925
+ private captureTextureData(layer_id: string): void {
926
+ const layer_textures =
927
+ this.context.layer_manager.get_layer_textures(layer_id);
928
+ if (!layer_textures) return;
929
+
930
+ try {
931
+ const original_texture = layer_textures.draw;
932
+
933
+ const texture_copy = RenderTexture.create({
934
+ width: original_texture.width,
935
+ height: original_texture.height,
936
+ resolution: window.devicePixelRatio || 1
937
+ });
938
+
939
+ const sprite = new Sprite(original_texture);
940
+
941
+ this.context.app.renderer.render(sprite, { renderTexture: texture_copy });
942
+
943
+ this.texture_copy = texture_copy;
944
+
945
+ sprite.destroy();
946
+ } catch (e) {
947
+ console.error("Failed to copy layer texture:", e);
948
+ this.texture_copy = null;
949
+ }
950
+ }
951
+
952
+ async execute(): Promise<void> {
953
+ this.context.layer_manager.delete_layer(this.layer_data.id);
954
+ }
955
+
956
+ async undo(): Promise<void> {
957
+ const dimensions = get(this.context.dimensions);
958
+
959
+ const container = this.context.layer_manager.create_layer({
960
+ width: dimensions.width,
961
+ height: dimensions.height,
962
+ layer_name: this.layer_data.name,
963
+ user_created: this.layer_data.user_created,
964
+ layer_id: this.layer_data.id,
965
+ make_active: this.layer_data.was_active
966
+ });
967
+
968
+ if (this.texture_copy) {
969
+ try {
970
+ const layer_textures = this.context.layer_manager.get_layer_textures(
971
+ this.layer_data.id
972
+ );
973
+ if (layer_textures) {
974
+ const sprite = new Sprite(this.texture_copy);
975
+
976
+ this.context.app.renderer.render(sprite, {
977
+ renderTexture: layer_textures.draw
978
+ });
979
+
980
+ sprite.destroy();
981
+ }
982
+ } catch (e) {
983
+ console.error("Failed to restore layer content:", e);
984
+ }
985
+ }
986
+
987
+ if (!this.layer_data.visible) {
988
+ this.context.layer_manager.toggle_layer_visibility(this.layer_data.id);
989
+ }
990
+
991
+ if (!this.layer_data.was_active && this.previous_active_layer) {
992
+ this.context.layer_manager.set_active_layer(this.previous_active_layer);
993
+ }
994
+ }
995
+
996
+ /**
997
+ * Clean up resources when the command is no longer needed
998
+ * This would need to be called by a garbage collection mechanism
999
+ */
1000
+ destroy(): void {
1001
+ if (this.texture_copy) {
1002
+ this.texture_copy.destroy();
1003
+ this.texture_copy = null;
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Command to reorder layers
1010
+ */
1011
+ export class ReorderLayerCommand implements Command {
1012
+ private original_order: string[];
1013
+ private new_order: string[];
1014
+ private layer_id: string;
1015
+ private direction: "up" | "down";
1016
+ name: string;
1017
+
1018
+ constructor(
1019
+ private context: ImageEditorContext,
1020
+ layer_id: string,
1021
+ direction: "up" | "down"
1022
+ ) {
1023
+ this.layer_id = layer_id;
1024
+ this.direction = direction;
1025
+ this.name = "ReorderLayer";
1026
+ const layers = this.context.layer_manager.get_layers();
1027
+ this.original_order = layers.map((l) => l.id);
1028
+
1029
+ const index = layers.findIndex((l) => l.id === layer_id);
1030
+ if (index === -1) {
1031
+ throw new Error(`Layer with ID ${layer_id} not found`);
1032
+ }
1033
+
1034
+ const new_index = direction === "up" ? index - 1 : index + 1;
1035
+ if (new_index < 0 || new_index >= layers.length) {
1036
+ this.new_order = [...this.original_order];
1037
+ } else {
1038
+ this.new_order = [...this.original_order];
1039
+ [this.new_order[index], this.new_order[new_index]] = [
1040
+ this.new_order[new_index],
1041
+ this.new_order[index]
1042
+ ];
1043
+ }
1044
+ }
1045
+
1046
+ async execute(): Promise<void> {
1047
+ this.context.layer_manager.move_layer(this.layer_id, this.direction);
1048
+ }
1049
+
1050
+ async undo(): Promise<void> {
1051
+ const current_layers = this.context.layer_manager.get_layers();
1052
+
1053
+ if (
1054
+ this.original_order.join(",") ===
1055
+ current_layers.map((l) => l.id).join(",")
1056
+ ) {
1057
+ return;
1058
+ }
1059
+
1060
+ const layer_index = current_layers.findIndex((l) => l.id === this.layer_id);
1061
+ if (layer_index === -1) return;
1062
+
1063
+ const original_index = this.original_order.indexOf(this.layer_id);
1064
+ const move_direction = layer_index > original_index ? "up" : "down";
1065
+
1066
+ this.context.layer_manager.move_layer(this.layer_id, move_direction);
1067
+ }
1068
+ }