@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.
- package/CHANGELOG.md +23 -0
- package/InteractiveImageEditor.svelte +4 -7
- package/dist/InteractiveImageEditor.svelte +4 -3
- package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
- package/dist/shared/Controls.svelte +32 -1
- package/dist/shared/Controls.svelte.d.ts +2 -0
- package/dist/shared/ImageEditor.svelte +23 -1
- package/dist/shared/ImageEditor.svelte.d.ts +2 -1
- package/dist/shared/brush/brush-cursor.js +0 -42
- package/dist/shared/brush/brush-textures.d.ts +19 -2
- package/dist/shared/brush/brush-textures.js +120 -82
- package/dist/shared/brush/brush.js +0 -52
- package/dist/shared/{utils → core}/commands.d.ts +19 -58
- package/dist/shared/core/commands.js +70 -0
- package/dist/shared/core/editor.d.ts +4 -91
- package/dist/shared/core/editor.js +18 -708
- package/dist/shared/core/layers.d.ts +143 -0
- package/dist/shared/core/layers.js +774 -0
- package/dist/shared/image/image.d.ts +14 -4
- package/dist/shared/image/image.js +91 -18
- package/dist/shared/resize/resize.d.ts +31 -0
- package/dist/shared/resize/resize.js +127 -290
- package/dist/shared/zoom/zoom.js +0 -51
- package/package.json +7 -7
- package/shared/Controls.svelte +33 -1
- package/shared/ImageEditor.svelte +25 -9
- package/shared/Toolbar.svelte +1 -4
- package/shared/brush/ColorSwatch.svelte +0 -3
- package/shared/brush/brush-cursor.ts +0 -42
- package/shared/brush/brush-textures.ts +166 -83
- package/shared/brush/brush.ts +0 -53
- package/shared/core/commands.ts +118 -0
- package/shared/core/editor.ts +33 -957
- package/shared/core/layers.ts +1068 -0
- package/shared/image/image.ts +139 -21
- package/shared/resize/resize.ts +160 -299
- package/shared/zoom/zoom.ts +0 -51
- package/dist/shared/utils/commands.js +0 -73
- 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
|
+
}
|