@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
@@ -8,6 +8,66 @@ import {
8
8
  } from "pixi.js";
9
9
  import { type ImageEditorContext, type Tool } from "../core/editor";
10
10
  import { type Tool as ToolbarTool, type Subtool } from "../Toolbar.svelte";
11
+ import { type Command } from "../core/commands";
12
+
13
+ /**
14
+ * Command that handles resizing the image and supports undo/redo
15
+ */
16
+ export class ResizeCommand implements Command {
17
+ private original_width: number;
18
+ private original_height: number;
19
+ private original_x: number;
20
+ private original_y: number;
21
+
22
+ private new_width: number;
23
+ private new_height: number;
24
+ private new_x: number;
25
+ private new_y: number;
26
+ private reset_ui: () => void;
27
+ name: string;
28
+
29
+ constructor(
30
+ private context: ImageEditorContext,
31
+ original_state: { width: number; height: number; x: number; y: number },
32
+ new_state: { width: number; height: number; x: number; y: number },
33
+ reset_ui: () => void
34
+ ) {
35
+ this.name = "Resize";
36
+ this.original_width = original_state.width;
37
+ this.original_height = original_state.height;
38
+ this.original_x = original_state.x;
39
+ this.original_y = original_state.y;
40
+
41
+ this.new_width = new_state.width;
42
+ this.new_height = new_state.height;
43
+ this.new_x = new_state.x;
44
+ this.new_y = new_state.y;
45
+ this.reset_ui = reset_ui;
46
+ }
47
+
48
+ async execute(): Promise<void> {
49
+ if (!this.context.background_image) return;
50
+
51
+ this.context.background_image.width = this.new_width;
52
+ this.context.background_image.height = this.new_height;
53
+ this.context.background_image.position.set(this.new_x, this.new_y);
54
+
55
+ this.reset_ui();
56
+ }
57
+
58
+ async undo(): Promise<void> {
59
+ if (!this.context.background_image) return;
60
+
61
+ this.context.background_image.width = this.original_width;
62
+ this.context.background_image.height = this.original_height;
63
+ this.context.background_image.position.set(
64
+ this.original_x,
65
+ this.original_y
66
+ );
67
+
68
+ this.reset_ui();
69
+ }
70
+ }
11
71
 
12
72
  /**
13
73
  * @class ResizeTool
@@ -20,15 +80,12 @@ export class ResizeTool implements Tool {
20
80
  current_tool: ToolbarTool = "image";
21
81
  current_subtool: Subtool = "size";
22
82
  private current_cursor: FederatedOptions["cursor"] = "unset";
23
- // Constants
24
83
  private readonly CORNER_SIZE = 25;
25
84
  private readonly LINE_THICKNESS = 5;
26
85
  private readonly HANDLE_COLOR = 0x000000;
27
86
  private readonly HIT_AREA_SIZE = 40;
28
- // Border region for outpainting
29
87
  private borderRegion = 0;
30
88
 
31
- // State
32
89
  private is_dragging = false;
33
90
  private selected_handle: Container | null = null;
34
91
  private active_corner_index = -1;
@@ -48,6 +105,13 @@ export class ResizeTool implements Tool {
48
105
  private event_callbacks: Map<string, (() => void)[]> = new Map();
49
106
  private last_scale = 1;
50
107
 
108
+ private original_state: {
109
+ width: number;
110
+ height: number;
111
+ x: number;
112
+ y: number;
113
+ } | null = null;
114
+
51
115
  /**
52
116
  * @method setup
53
117
  * @async
@@ -66,29 +130,21 @@ export class ResizeTool implements Tool {
66
130
  this.current_tool = tool;
67
131
  this.current_subtool = subtool;
68
132
 
69
- // Get the border region from context if it exists
70
133
  if (context.background_image) {
71
- // First try to read from a custom property on the background image
72
- // This is important as the position might have been adjusted
73
134
  if ((context.background_image as any).borderRegion !== undefined) {
74
135
  this.borderRegion = (context.background_image as any).borderRegion;
75
136
  } else {
76
- // Fall back to the position-based detection as before
77
- // But add additional checks to make sure it's not just centering
78
137
  const bg = context.background_image;
79
138
  const container = context.image_container;
80
139
 
81
- // Check if the image position seems intentional (not just centered)
82
140
  const isHorizontallyCentered =
83
141
  Math.abs(bg.position.x - (container.width - bg.width) / 2) < 2;
84
142
  const isVerticallyCentered =
85
143
  Math.abs(bg.position.y - (container.height - bg.height) / 2) < 2;
86
144
 
87
145
  if (!isHorizontallyCentered || !isVerticallyCentered) {
88
- // If not centered, this is likely an intentional border
89
146
  this.borderRegion = Math.max(bg.position.x, bg.position.y);
90
147
 
91
- // Store it for future reference
92
148
  (context.background_image as any).borderRegion = this.borderRegion;
93
149
  }
94
150
  }
@@ -96,7 +152,6 @@ export class ResizeTool implements Tool {
96
152
 
97
153
  context.dimensions.subscribe((dimensions) => {
98
154
  this.dimensions = dimensions;
99
- // Force UI update if it exists
100
155
  if (this.resize_ui_container) {
101
156
  this.update_resize_ui();
102
157
  }
@@ -104,7 +159,6 @@ export class ResizeTool implements Tool {
104
159
 
105
160
  context.position.subscribe((position) => {
106
161
  this.position = position;
107
- // Update resize UI immediately when position changes to avoid lag
108
162
  if (
109
163
  this.resize_ui_container &&
110
164
  this.current_tool === "image" &&
@@ -116,7 +170,6 @@ export class ResizeTool implements Tool {
116
170
 
117
171
  context.scale.subscribe((scale) => {
118
172
  this.scale = scale;
119
- // Update resize UI immediately when scale changes to avoid lag
120
173
  if (
121
174
  this.resize_ui_container &&
122
175
  this.current_tool === "image" &&
@@ -126,11 +179,9 @@ export class ResizeTool implements Tool {
126
179
  }
127
180
  });
128
181
 
129
- // Initialize resize UI
130
182
  await this.init_resize_ui();
131
183
  this.setup_event_listeners();
132
184
 
133
- // Apply boundary constraints only if the image is outside the container boundaries
134
185
  if (
135
186
  this.current_subtool === "size" &&
136
187
  this.image_editor_context.background_image
@@ -144,7 +195,6 @@ export class ResizeTool implements Tool {
144
195
  const container_width = container_bounds.width;
145
196
  const container_height = container_bounds.height;
146
197
 
147
- // Check if the image exceeds the container boundaries
148
198
  const min_x = -(bg_width - container_width);
149
199
  const min_y = -(bg_height - container_height);
150
200
  const max_x = 0;
@@ -157,7 +207,6 @@ export class ResizeTool implements Tool {
157
207
  bg_height > container_height &&
158
208
  (bg.position.y < min_y || bg.position.y > max_y);
159
209
 
160
- // Only apply constraints if the image is out of bounds
161
210
  if (x_out_of_bounds || y_out_of_bounds) {
162
211
  this.apply_boundary_constraints();
163
212
  }
@@ -173,7 +222,6 @@ export class ResizeTool implements Tool {
173
222
  if (this.resize_ui_container) {
174
223
  this.image_editor_context.app.stage.removeChild(this.resize_ui_container);
175
224
  }
176
- // Remove event listeners
177
225
  this.cleanup_event_listeners();
178
226
  }
179
227
 
@@ -189,21 +237,17 @@ export class ResizeTool implements Tool {
189
237
  this.current_subtool = subtool;
190
238
 
191
239
  if (tool === "image" && subtool === "size") {
192
- // Check if there's a stored border region on the background image
193
240
  if (this.image_editor_context.background_image) {
194
241
  const storedBorderRegion = (
195
242
  this.image_editor_context.background_image as any
196
243
  ).borderRegion;
197
244
  if (typeof storedBorderRegion === "number" && storedBorderRegion > 0) {
198
- // Use the stored border region
199
245
  this.borderRegion = storedBorderRegion;
200
246
  }
201
247
  }
202
248
 
203
249
  this.show_resize_ui();
204
250
 
205
- // Apply boundary constraints when switching to the resize tool
206
- // but only if the image is outside the container boundaries
207
251
  if (this.image_editor_context.background_image) {
208
252
  const bg = this.image_editor_context.background_image;
209
253
  const container = this.image_editor_context.image_container;
@@ -214,11 +258,9 @@ export class ResizeTool implements Tool {
214
258
  const container_width = container_bounds.width;
215
259
  const container_height = container_bounds.height;
216
260
 
217
- // Apply effective width/height calculations with border region
218
261
  const effectiveWidth = container_width - this.borderRegion * 2;
219
262
  const effectiveHeight = container_height - this.borderRegion * 2;
220
263
 
221
- // Check if the image exceeds the effective container area or intrudes on border
222
264
  const min_x =
223
265
  bg_width > effectiveWidth
224
266
  ? -(bg_width - effectiveWidth) + this.borderRegion
@@ -237,11 +279,9 @@ export class ResizeTool implements Tool {
237
279
  ? this.borderRegion
238
280
  : container_height - bg_height - this.borderRegion;
239
281
 
240
- // Check if any constraints are violated
241
282
  const x_out_of_bounds = bg.position.x < min_x || bg.position.x > max_x;
242
283
  const y_out_of_bounds = bg.position.y < min_y || bg.position.y > max_y;
243
284
 
244
- // Only apply constraints if the image is out of bounds
245
285
  if (x_out_of_bounds || y_out_of_bounds) {
246
286
  this.apply_boundary_constraints();
247
287
  }
@@ -259,23 +299,16 @@ export class ResizeTool implements Tool {
259
299
  * @returns {Promise<void>}
260
300
  */
261
301
  private async init_resize_ui(): Promise<void> {
262
- // Create resize UI with scaled dimensions
263
302
  this.resize_ui_container = this.make_resize_ui(
264
303
  this.dimensions.width * this.scale,
265
304
  this.dimensions.height * this.scale
266
305
  );
267
306
 
268
- // Position the resize UI container at the image position
269
307
  this.resize_ui_container.position.set(this.position.x, this.position.y);
270
308
 
271
- // Initially hide the resize UI until explicitly enabled by set_tool
272
309
  this.resize_ui_container.visible = false;
273
310
 
274
311
  this.image_editor_context.app.stage.addChild(this.resize_ui_container);
275
-
276
- // Instead of using the ticker for continuous updates, we'll use immediate updates
277
- // on scale and position changes, which we've added in the setup method
278
- // This will eliminate lag during zoom operations
279
312
  }
280
313
 
281
314
  /**
@@ -291,7 +324,6 @@ export class ResizeTool implements Tool {
291
324
  resize_container.eventMode = "static";
292
325
  resize_container.interactiveChildren = true;
293
326
 
294
- // Create solid outline (will be mostly hidden by the dotted outline)
295
327
  const outline = new Graphics().rect(0, 0, width, height).stroke({
296
328
  width: 1,
297
329
  color: 0x000000,
@@ -299,10 +331,8 @@ export class ResizeTool implements Tool {
299
331
  alpha: 0.3
300
332
  });
301
333
 
302
- // Add a second graphics object for the dotted effect
303
334
  const dotted_outline = new Graphics();
304
335
 
305
- // Use pixelLine for crisp rendering
306
336
  dotted_outline.rect(0, 0, width, height).stroke({
307
337
  width: 1,
308
338
  color: 0x000000,
@@ -310,34 +340,25 @@ export class ResizeTool implements Tool {
310
340
  pixelLine: true
311
341
  });
312
342
 
313
- // Create dashed line effect by drawing small rectangles along the border
314
343
  const dash_length = 5;
315
344
  const gap_length = 5;
316
345
  const total_length = dash_length + gap_length;
317
346
 
318
- // Draw dashed horizontal lines (top and bottom) - draw fewer dashes for performance
319
347
  for (let x = 0; x < width; x += total_length * 2) {
320
- // Skip every other dash
321
- // Top dash
322
348
  dotted_outline
323
349
  .rect(x, 0, Math.min(dash_length, width - x), 1)
324
350
  .fill(0x000000);
325
351
 
326
- // Bottom dash
327
352
  dotted_outline
328
353
  .rect(x, height - 1, Math.min(dash_length, width - x), 1)
329
354
  .fill(0x000000);
330
355
  }
331
356
 
332
- // Draw dashed vertical lines (left and right) - draw fewer dashes for performance
333
357
  for (let y = 0; y < height; y += total_length * 2) {
334
- // Skip every other dash
335
- // Left dash
336
358
  dotted_outline
337
359
  .rect(0, y, 1, Math.min(dash_length, height - y))
338
360
  .fill(0x000000);
339
361
 
340
- // Right dash
341
362
  dotted_outline
342
363
  .rect(width - 1, y, 1, Math.min(dash_length, height - y))
343
364
  .fill(0x000000);
@@ -346,18 +367,13 @@ export class ResizeTool implements Tool {
346
367
  resize_container.addChild(outline);
347
368
  resize_container.addChild(dotted_outline);
348
369
 
349
- // Create a transparent interactive area for the move cursor
350
370
  const move_area = new Graphics()
351
371
  .rect(0, 0, width, height)
352
- .fill(0xffffff, 0); // Transparent fill
353
-
372
+ .fill(0xffffff, 0);
354
373
  move_area.eventMode = "static";
355
- move_area.cursor = "move"; // Set cursor to move
356
-
357
- // Add the move area before the handles so it doesn't interfere with handle interactions
374
+ move_area.cursor = "move";
358
375
  resize_container.addChild(move_area);
359
376
 
360
- // Create handles
361
377
  this.create_corner_handles(resize_container, width, height);
362
378
  this.create_edge_handles(resize_container, width, height);
363
379
 
@@ -377,17 +393,13 @@ export class ResizeTool implements Tool {
377
393
  handle.cursor = "pointer";
378
394
 
379
395
  const handle_graphics = new Graphics();
380
- const handle_size = is_edge ? 8 : 10; // Smaller for edge handles
381
-
382
- // Create square handles
396
+ const handle_size = is_edge ? 8 : 10;
383
397
  handle_graphics
384
398
  .rect(-handle_size / 2, -handle_size / 2, handle_size, handle_size)
385
- .fill(0xffffff) // White fill
386
- .stroke({ width: 1, color: this.HANDLE_COLOR }); // Black border
387
-
399
+ .fill(0xffffff)
400
+ .stroke({ width: 1, color: this.HANDLE_COLOR });
388
401
  handle.addChild(handle_graphics);
389
402
 
390
- // Adjust hit area based on handle type
391
403
  const hit_size = is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE;
392
404
  handle.hitArea = new Rectangle(
393
405
  -hit_size / 2,
@@ -413,16 +425,14 @@ export class ResizeTool implements Tool {
413
425
  width: number,
414
426
  height: number
415
427
  ): void {
416
- // Horizontal handles at top and bottom
417
428
  [-1, 1].forEach((i, index) => {
418
- const handle = this.create_handle(true); // true for edge handle
429
+ const handle = this.create_handle(true);
419
430
  handle.rotation = 0;
420
431
 
421
432
  handle.on("pointerdown", (event: FederatedPointerEvent) => {
422
- this.handle_pointer_down(event, handle, -1, index); // 0 for top, 1 for bottom
433
+ this.handle_pointer_down(event, handle, -1, index);
423
434
  });
424
435
 
425
- // Center the handle
426
436
  handle.x = width / 2;
427
437
  handle.y = i < 0 ? 0 : height;
428
438
  handle.cursor = "ns-resize";
@@ -430,16 +440,13 @@ export class ResizeTool implements Tool {
430
440
  container.addChild(handle);
431
441
  });
432
442
 
433
- // Vertical handles at left and right
434
443
  [-1, 1].forEach((i, index) => {
435
- const handle = this.create_handle(true); // true for edge handle
436
- handle.rotation = 0; // No rotation needed for square handles
437
-
444
+ const handle = this.create_handle(true);
445
+ handle.rotation = 0;
438
446
  handle.on("pointerdown", (event: FederatedPointerEvent) => {
439
- this.handle_pointer_down(event, handle, -1, index + 2); // 2 for left, 3 for right
447
+ this.handle_pointer_down(event, handle, -1, index + 2);
440
448
  });
441
449
 
442
- // Center the handle
443
450
  handle.x = i < 0 ? 0 : width;
444
451
  handle.y = height / 2;
445
452
  handle.cursor = "ew-resize";
@@ -462,14 +469,14 @@ export class ResizeTool implements Tool {
462
469
  height: number
463
470
  ): void {
464
471
  const corners = [
465
- { x: 0, y: 0, cursor: "nwse-resize" }, // Top-left
466
- { x: width, y: 0, cursor: "nesw-resize" }, // Top-right
467
- { x: 0, y: height, cursor: "nesw-resize" }, // Bottom-left
468
- { x: width, y: height, cursor: "nwse-resize" } // Bottom-right
472
+ { x: 0, y: 0, cursor: "nwse-resize" },
473
+ { x: width, y: 0, cursor: "nesw-resize" },
474
+ { x: 0, y: height, cursor: "nesw-resize" },
475
+ { x: width, y: height, cursor: "nwse-resize" }
469
476
  ];
470
477
 
471
478
  corners.forEach(({ x, y, cursor }, i) => {
472
- const corner = this.create_handle(false); // false for corner handle
479
+ const corner = this.create_handle(false);
473
480
  corner.x = x;
474
481
  corner.y = y;
475
482
 
@@ -499,7 +506,6 @@ export class ResizeTool implements Tool {
499
506
  corner_index: number,
500
507
  edge_index: number
501
508
  ): void {
502
- // Only handle resize events if resize tool is active
503
509
  if (this.current_subtool !== "size") return;
504
510
 
505
511
  event.stopPropagation();
@@ -509,11 +515,20 @@ export class ResizeTool implements Tool {
509
515
  this.active_corner_index = corner_index;
510
516
  this.active_edge_index = edge_index;
511
517
 
512
- // Convert global position to the container's local coordinates.
513
518
  const local_pos = this.image_editor_context.image_container.toLocal(
514
519
  event.global
515
520
  );
516
521
  this.last_pointer_position = new Point(local_pos.x, local_pos.y);
522
+
523
+ if (this.image_editor_context.background_image) {
524
+ const bg = this.image_editor_context.background_image;
525
+ this.original_state = {
526
+ width: bg.width,
527
+ height: bg.height,
528
+ x: bg.position.x,
529
+ y: bg.position.y
530
+ };
531
+ }
517
532
  }
518
533
 
519
534
  /**
@@ -530,23 +545,14 @@ export class ResizeTool implements Tool {
530
545
  original_aspect_ratio: number,
531
546
  delta: Point
532
547
  ): { width: number; height: number } {
533
- // For corner resizing, use the dimension that the user is actively changing
534
- // This makes the resize follow the mouse cursor more naturally
535
-
536
- // Determine which dimension changed more significantly
537
548
  const abs_delta_x = Math.abs(delta.x);
538
549
  const abs_delta_y = Math.abs(delta.y);
539
550
 
540
- // Use a more responsive approach to determine which dimension to prioritize
541
- // This makes the resize follow the mouse cursor more closely
542
551
  if (abs_delta_x > abs_delta_y * 1.2) {
543
- // User is moving more horizontally, so prioritize width
544
552
  new_height = new_width / original_aspect_ratio;
545
553
  } else if (abs_delta_y > abs_delta_x * 1.2) {
546
- // User is moving more vertically, so prioritize height
547
554
  new_width = new_height * original_aspect_ratio;
548
555
  } else {
549
- // User is moving diagonally, use the larger delta for better control
550
556
  if (new_width / original_aspect_ratio > new_height) {
551
557
  new_height = new_width / original_aspect_ratio;
552
558
  } else {
@@ -569,7 +575,6 @@ export class ResizeTool implements Tool {
569
575
  height: number,
570
576
  maintain_aspect_ratio: boolean
571
577
  ): { width: number; height: number } {
572
- // Get container dimensions - restrict to container size
573
578
  const max_width = this.dimensions.width;
574
579
  const max_height = this.dimensions.height;
575
580
 
@@ -577,7 +582,6 @@ export class ResizeTool implements Tool {
577
582
  let new_height = height;
578
583
 
579
584
  if (new_width > max_width) {
580
- // If maintaining aspect ratio, adjust height proportionally
581
585
  if (maintain_aspect_ratio && this.active_corner_index !== -1) {
582
586
  new_height = (max_width / new_width) * new_height;
583
587
  }
@@ -585,14 +589,12 @@ export class ResizeTool implements Tool {
585
589
  }
586
590
 
587
591
  if (new_height > max_height) {
588
- // If maintaining aspect ratio, adjust width proportionally
589
592
  if (maintain_aspect_ratio && this.active_corner_index !== -1) {
590
593
  new_width = (max_height / new_height) * new_width;
591
594
  }
592
595
  new_height = max_height;
593
596
  }
594
597
 
595
- // Ensure minimum size
596
598
  new_width = Math.max(20, new_width);
597
599
  new_height = Math.max(20, new_height);
598
600
 
@@ -616,15 +618,11 @@ export class ResizeTool implements Tool {
616
618
  let delta_x = 0;
617
619
  let delta_y = 0;
618
620
 
619
- // Calculate position deltas based on which handle is being dragged
620
621
  if (this.active_corner_index === 0 || this.active_edge_index === 2) {
621
- // Left side (top-left corner or left edge)
622
622
  delta_x = old_width - new_width;
623
623
  } else if (this.active_corner_index === 2) {
624
- // Bottom-left corner should anchor to top-right
625
- // So we need to adjust both x and y
626
624
  delta_x = old_width - new_width;
627
- delta_y = 0; // No y adjustment to keep it anchored to the top
625
+ delta_y = 0;
628
626
  }
629
627
 
630
628
  if (
@@ -632,7 +630,6 @@ export class ResizeTool implements Tool {
632
630
  this.active_corner_index === 1 ||
633
631
  this.active_edge_index === 0
634
632
  ) {
635
- // Top side (top-left, top-right corners or top edge)
636
633
  delta_y = old_height - new_height;
637
634
  }
638
635
 
@@ -649,55 +646,43 @@ export class ResizeTool implements Tool {
649
646
  const bg = this.image_editor_context.background_image;
650
647
  const image_container = this.image_editor_context.image_container;
651
648
 
652
- // Get current position
653
649
  let new_x = bg.position.x;
654
650
  let new_y = bg.position.y;
655
651
 
656
- // Apply boundary constraints to keep the image within the container
657
652
  const container_width = image_container.width;
658
653
  const container_height = image_container.height;
659
654
  const bg_width = bg.width;
660
655
  const bg_height = bg.height;
661
656
 
662
- // For images larger than the effective container area (container minus border),
663
- // constrain them to maintain the border region
664
657
  const effectiveWidth = container_width - this.borderRegion * 2;
665
658
  const effectiveHeight = container_height - this.borderRegion * 2;
666
659
 
667
660
  if (bg_width > effectiveWidth) {
668
- // Calculate the minimum and maximum allowed positions
669
661
  const min_x = -(bg_width - effectiveWidth) + this.borderRegion;
670
662
  const max_x = this.borderRegion;
671
663
  new_x = Math.max(min_x, Math.min(max_x, new_x));
672
664
  } else {
673
- // If image is smaller than effective container, enforce at least the border region on each side
674
665
  const min_x = this.borderRegion;
675
666
  const max_x = container_width - bg_width - this.borderRegion;
676
- // Only constrain if the image would intrude on the border region
677
667
  if (new_x < min_x || new_x > max_x) {
678
668
  new_x = Math.max(min_x, Math.min(max_x, new_x));
679
669
  }
680
670
  }
681
671
 
682
672
  if (bg_height > effectiveHeight) {
683
- // Calculate the minimum and maximum allowed positions
684
673
  const min_y = -(bg_height - effectiveHeight) + this.borderRegion;
685
674
  const max_y = this.borderRegion;
686
675
  new_y = Math.max(min_y, Math.min(max_y, new_y));
687
676
  } else {
688
- // If image is smaller than effective container, enforce at least the border region on each side
689
677
  const min_y = this.borderRegion;
690
678
  const max_y = container_height - bg_height - this.borderRegion;
691
- // Only constrain if the image would intrude on the border region
692
679
  if (new_y < min_y || new_y > max_y) {
693
680
  new_y = Math.max(min_y, Math.min(max_y, new_y));
694
681
  }
695
682
  }
696
683
 
697
- // Apply the constrained position
698
684
  bg.position.set(new_x, new_y);
699
685
 
700
- // Update resize UI with container position and scaled sprite position
701
686
  if (this.resize_ui_container) {
702
687
  const total_x = image_container.position.x + new_x * this.scale;
703
688
  const total_y = image_container.position.y + new_y * this.scale;
@@ -714,43 +699,39 @@ export class ResizeTool implements Tool {
714
699
  delta: Point,
715
700
  maintain_aspect_ratio = false
716
701
  ): void {
717
- // The delta is already in local coordinates, no need to scale it further
718
702
  const background_image = this.image_editor_context.background_image;
719
703
 
720
704
  if (!background_image) return;
721
705
 
722
- // Store original dimensions and position
723
706
  const original_width = background_image.width;
724
707
  const original_height = background_image.height;
725
708
  const original_x = background_image.position.x;
726
709
  const original_y = background_image.position.y;
727
710
  const original_aspect_ratio = original_width / original_height;
728
711
 
729
- // Calculate current position based on the delta and the active handle
730
712
  let current_position: Point;
731
713
 
732
714
  if (this.active_corner_index !== -1) {
733
- // For corners, calculate the current position based on the corner and delta
734
715
  switch (this.active_corner_index) {
735
- case 0: // Top-left
716
+ case 0:
736
717
  current_position = new Point(
737
718
  original_x - delta.x,
738
719
  original_y - delta.y
739
720
  );
740
721
  break;
741
- case 1: // Top-right
722
+ case 1:
742
723
  current_position = new Point(
743
724
  original_x + original_width + delta.x,
744
725
  original_y - delta.y
745
726
  );
746
727
  break;
747
- case 2: // Bottom-left
728
+ case 2:
748
729
  current_position = new Point(
749
730
  original_x - delta.x,
750
731
  original_y + original_height + delta.y
751
732
  );
752
733
  break;
753
- case 3: // Bottom-right
734
+ case 3:
754
735
  current_position = new Point(
755
736
  original_x + original_width + delta.x,
756
737
  original_y + original_height + delta.y
@@ -760,7 +741,6 @@ export class ResizeTool implements Tool {
760
741
  return;
761
742
  }
762
743
 
763
- // Use the existing corner resize logic
764
744
  const dimensions = this.handle_direct_corner_resize(
765
745
  current_position,
766
746
  original_width,
@@ -771,32 +751,30 @@ export class ResizeTool implements Tool {
771
751
  maintain_aspect_ratio
772
752
  );
773
753
 
774
- // Apply the new dimensions and position
775
754
  background_image.width = dimensions.width;
776
755
  background_image.height = dimensions.height;
777
756
  background_image.position.set(dimensions.x, dimensions.y);
778
757
  } else if (this.active_edge_index !== -1) {
779
- // For edges, calculate the current position based on the edge and delta
780
758
  switch (this.active_edge_index) {
781
- case 0: // Top
759
+ case 0:
782
760
  current_position = new Point(
783
761
  original_x + original_width / 2,
784
762
  original_y - delta.y
785
763
  );
786
764
  break;
787
- case 1: // Bottom
765
+ case 1:
788
766
  current_position = new Point(
789
767
  original_x + original_width / 2,
790
768
  original_y + original_height + delta.y
791
769
  );
792
770
  break;
793
- case 2: // Left
771
+ case 2:
794
772
  current_position = new Point(
795
773
  original_x - delta.x,
796
774
  original_y + original_height / 2
797
775
  );
798
776
  break;
799
- case 3: // Right
777
+ case 3:
800
778
  current_position = new Point(
801
779
  original_x + original_width + delta.x,
802
780
  original_y + original_height / 2
@@ -806,7 +784,6 @@ export class ResizeTool implements Tool {
806
784
  return;
807
785
  }
808
786
 
809
- // Use the existing edge resize logic
810
787
  const dimensions = this.handle_direct_edge_resize(
811
788
  current_position,
812
789
  original_width,
@@ -815,16 +792,11 @@ export class ResizeTool implements Tool {
815
792
  original_y
816
793
  );
817
794
 
818
- // Apply the new dimensions and position
819
795
  background_image.width = dimensions.width;
820
796
  background_image.height = dimensions.height;
821
797
  background_image.position.set(dimensions.x, dimensions.y);
822
798
  }
823
799
 
824
- // We don't apply boundary constraints during resizing
825
- // This will be done when the user releases the mouse in handle_pointer_up
826
-
827
- // Update the resize UI to match the new position and dimensions
828
800
  this.update_resize_ui();
829
801
  }
830
802
 
@@ -842,36 +814,28 @@ export class ResizeTool implements Tool {
842
814
  const background_image = this.image_editor_context.background_image;
843
815
  const image_container = this.image_editor_context.image_container;
844
816
 
845
- // Get current image container global position
846
817
  const container_global_pos = image_container.getGlobalPosition();
847
818
 
848
- // Calculate background image position in global coordinates
849
819
  const bg_pos_x = background_image.position.x * this.scale;
850
820
  const bg_pos_y = background_image.position.y * this.scale;
851
821
 
852
- // Position resize UI precisely to match the image position in global space
853
822
  this.resize_ui_container.position.set(
854
823
  container_global_pos.x + bg_pos_x,
855
824
  container_global_pos.y + bg_pos_y
856
825
  );
857
826
 
858
- // Get the scaled dimensions
859
827
  const scaled_width = background_image.width * this.scale;
860
828
  const scaled_height = background_image.height * this.scale;
861
829
 
862
- // Redraw the outline so it matches the resize_bounds size
863
830
  const outline = this.resize_ui_container.getChildAt(0) as Graphics;
864
831
  outline
865
832
  .clear()
866
833
  .rect(0, 0, scaled_width, scaled_height)
867
834
  .stroke({ width: 1, color: 0x000000, alignment: 0, alpha: 0.3 });
868
835
 
869
- // Redraw the dotted outline
870
836
  const dotted_outline = this.resize_ui_container.getChildAt(1) as Graphics;
871
837
  dotted_outline.clear();
872
838
 
873
- // Use simpler more performant approach for the dotted outline
874
- // Just draw the solid outline with pixelLine for crisp rendering
875
839
  dotted_outline.rect(0, 0, scaled_width, scaled_height).stroke({
876
840
  width: 1,
877
841
  color: 0x000000,
@@ -879,27 +843,20 @@ export class ResizeTool implements Tool {
879
843
  pixelLine: true
880
844
  });
881
845
 
882
- // Only draw dashes when not actively resizing or moving to improve performance
883
- // And disable dashes during zoom operations to prevent lag
884
846
  if (
885
847
  !this.is_dragging &&
886
848
  !this.is_moving &&
887
849
  Math.abs(this.scale - this.last_scale) < 0.001
888
850
  ) {
889
- // Create dashed line effect by drawing small rectangles along the border
890
851
  const dash_length = 5;
891
852
  const gap_length = 5;
892
853
  const total_length = dash_length + gap_length;
893
854
 
894
- // Draw dashed horizontal lines (top and bottom) - draw fewer dashes for performance
895
855
  for (let x = 0; x < scaled_width; x += total_length * 2) {
896
- // Skip every other dash
897
- // Top dash
898
856
  dotted_outline
899
857
  .rect(x, 0, Math.min(dash_length, scaled_width - x), 1)
900
858
  .fill(0x000000);
901
859
 
902
- // Bottom dash
903
860
  dotted_outline
904
861
  .rect(
905
862
  x,
@@ -910,15 +867,11 @@ export class ResizeTool implements Tool {
910
867
  .fill(0x000000);
911
868
  }
912
869
 
913
- // Draw dashed vertical lines (left and right) - draw fewer dashes for performance
914
870
  for (let y = 0; y < scaled_height; y += total_length * 2) {
915
- // Skip every other dash
916
- // Left dash
917
871
  dotted_outline
918
872
  .rect(0, y, 1, Math.min(dash_length, scaled_height - y))
919
873
  .fill(0x000000);
920
874
 
921
- // Right dash
922
875
  dotted_outline
923
876
  .rect(
924
877
  scaled_width - 1,
@@ -930,14 +883,11 @@ export class ResizeTool implements Tool {
930
883
  }
931
884
  }
932
885
 
933
- // Store the current scale for comparison on next update
934
886
  this.last_scale = this.scale;
935
887
 
936
- // Update the move area
937
888
  const move_area = this.resize_ui_container.getChildAt(2) as Graphics;
938
889
  move_area.clear().rect(0, 0, scaled_width, scaled_height).fill(0xffffff, 0);
939
890
 
940
- // Update handle positions to reflect the resized area
941
891
  this.update_handle_positions(scaled_width, scaled_height);
942
892
  }
943
893
 
@@ -949,34 +899,28 @@ export class ResizeTool implements Tool {
949
899
  private update_handle_positions(width: number, height: number): void {
950
900
  if (!this.resize_ui_container) return;
951
901
 
952
- // Skip the outline, dotted outline, and move area (index 0, 1, and 2), get corner/edge handles
953
902
  const handles = this.resize_ui_container.children.slice(3);
954
903
 
955
- // Update the move area size
956
904
  const move_area = this.resize_ui_container.getChildAt(2) as Graphics;
957
905
  move_area.clear().rect(0, 0, width, height).fill(0xffffff, 0);
958
906
 
959
- // First 4 are corners
960
907
  const corners = handles.slice(0, 4);
961
908
  const cornerPositions = [
962
- { x: 0, y: 0 }, // Top-left
963
- { x: width, y: 0 }, // Top-right
964
- { x: 0, y: height }, // Bottom-left
965
- { x: width, y: height } // Bottom-right
909
+ { x: 0, y: 0 },
910
+ { x: width, y: 0 },
911
+ { x: 0, y: height },
912
+ { x: width, y: height }
966
913
  ];
967
914
 
968
915
  corners.forEach((handle, i) => {
969
916
  handle.position.set(cornerPositions[i].x, cornerPositions[i].y);
970
917
  });
971
918
 
972
- // Remaining 4 are edges
973
919
  const edges = handles.slice(4);
974
920
  edges.forEach((handle, i) => {
975
921
  if (i < 2) {
976
- // Top/Bottom edges
977
922
  handle.position.set(width / 2, i === 0 ? 0 : height);
978
923
  } else {
979
- // Left/Right edges
980
924
  handle.position.set(i === 2 ? 0 : width, height / 2);
981
925
  }
982
926
  });
@@ -992,7 +936,6 @@ export class ResizeTool implements Tool {
992
936
  stage.on("pointerup", this.handle_pointer_up.bind(this));
993
937
  stage.on("pointerupoutside", this.handle_pointer_up.bind(this));
994
938
 
995
- // Set up direct DOM event listeners for dragging the background image
996
939
  this.setup_dom_event_listeners();
997
940
  }
998
941
 
@@ -1000,113 +943,120 @@ export class ResizeTool implements Tool {
1000
943
  * Sets up direct DOM event listeners for dragging the background image
1001
944
  */
1002
945
  private setup_dom_event_listeners(): void {
1003
- // Clean up any existing DOM event listeners
1004
946
  this.cleanup_dom_event_listeners();
1005
947
 
1006
948
  const canvas = this.image_editor_context.app.canvas;
1007
949
 
1008
- // Define the mousedown handler
1009
950
  this.dom_mousedown_handler = (e: MouseEvent) => {
1010
- // Only process if we're in the size subtool
1011
951
  if (this.current_subtool !== "size") return;
1012
952
 
1013
- // Check if we're already dragging a handle (resize operation)
1014
953
  if (this.is_dragging || this.selected_handle) return;
1015
954
 
1016
- // Check if we have a background image
1017
955
  if (!this.image_editor_context.background_image) return;
1018
956
 
1019
957
  const bg = this.image_editor_context.background_image;
1020
958
 
1021
- // Get container and background image bounds
1022
959
  const container = this.image_editor_context.image_container;
1023
960
  const containerBounds = container.getBounds();
1024
961
 
1025
- // Get mouse position relative to the canvas
1026
962
  const rect = canvas.getBoundingClientRect();
1027
963
  const x = e.clientX - rect.left;
1028
964
  const y = e.clientY - rect.top;
1029
965
 
1030
- // Convert to local coordinates within the container
1031
966
  const localX = (x - containerBounds.x) / this.scale;
1032
967
  const localY = (y - containerBounds.y) / this.scale;
1033
968
 
1034
- // Check if the click is within the background image bounds using local coordinates
1035
969
  if (
1036
970
  localX >= bg.position.x &&
1037
971
  localX <= bg.position.x + bg.width &&
1038
972
  localY >= bg.position.y &&
1039
973
  localY <= bg.position.y + bg.height
1040
974
  ) {
1041
- // Set the moving flag to true to indicate we're dragging the image
1042
975
  this.is_moving = true;
1043
976
 
1044
- // Store the initial pointer position in local coordinates
1045
977
  this.last_pointer_position = new Point(localX, localY);
1046
978
 
1047
- // Prevent default behavior and stop propagation
979
+ this.original_state = {
980
+ width: bg.width,
981
+ height: bg.height,
982
+ x: bg.position.x,
983
+ y: bg.position.y
984
+ };
985
+
1048
986
  e.preventDefault();
1049
987
  e.stopPropagation();
1050
988
  }
1051
989
  };
1052
990
 
1053
- // Define the mousemove handler
1054
991
  this.dom_mousemove_handler = (e: MouseEvent) => {
1055
- // Only process if we're in the size subtool and we're moving the image
1056
992
  if (this.current_subtool !== "size" || !this.is_moving) return;
1057
993
 
1058
- // Check if we have the necessary state
1059
994
  if (
1060
995
  !this.last_pointer_position ||
1061
996
  !this.image_editor_context.background_image
1062
997
  )
1063
998
  return;
1064
999
 
1065
- // Get container bounds
1066
1000
  const container = this.image_editor_context.image_container;
1067
1001
  const containerBounds = container.getBounds();
1068
1002
 
1069
- // Get mouse position relative to the canvas
1070
1003
  const rect = canvas.getBoundingClientRect();
1071
1004
  const x = e.clientX - rect.left;
1072
1005
  const y = e.clientY - rect.top;
1073
1006
 
1074
- // Convert to local coordinates within the container
1075
1007
  const localX = (x - containerBounds.x) / this.scale;
1076
1008
  const localY = (y - containerBounds.y) / this.scale;
1077
1009
 
1078
- // Create a current position point
1079
1010
  const current_position = new Point(localX, localY);
1080
1011
 
1081
- // Handle image dragging
1082
1012
  this.handle_image_dragging(current_position);
1083
1013
 
1084
- // Update the last pointer position for continuous dragging
1085
1014
  this.last_pointer_position = current_position;
1086
1015
 
1087
- // Prevent default behavior and stop propagation
1088
1016
  e.preventDefault();
1089
1017
  e.stopPropagation();
1090
1018
  };
1091
1019
 
1092
- // Define the mouseup handler
1093
1020
  this.dom_mouseup_handler = (e: MouseEvent) => {
1094
- // Only process if we're in the size subtool and we're moving the image
1095
- if (this.current_subtool !== "size" || !this.is_moving) return;
1021
+ if (this.current_subtool !== "size") return;
1022
+
1023
+ if (this.original_state && this.image_editor_context.background_image) {
1024
+ const bg = this.image_editor_context.background_image;
1025
+ const new_state = {
1026
+ width: bg.width,
1027
+ height: bg.height,
1028
+ x: bg.position.x,
1029
+ y: bg.position.y
1030
+ };
1031
+
1032
+ if (
1033
+ this.original_state.width !== new_state.width ||
1034
+ this.original_state.height !== new_state.height ||
1035
+ this.original_state.x !== new_state.x ||
1036
+ this.original_state.y !== new_state.y
1037
+ ) {
1038
+ const resize_command = new ResizeCommand(
1039
+ this.image_editor_context,
1040
+ this.original_state,
1041
+ new_state,
1042
+ this.update_resize_ui.bind(this)
1043
+ );
1044
+
1045
+ this.image_editor_context.execute_command(resize_command);
1046
+ }
1047
+
1048
+ this.original_state = null;
1049
+ }
1096
1050
 
1097
- // Reset all state variables
1098
1051
  this.is_moving = false;
1099
1052
  this.last_pointer_position = null;
1100
1053
 
1101
- // Update the resize UI without applying constraints
1102
1054
  this.update_resize_ui();
1103
1055
 
1104
- // Prevent default behavior and stop propagation
1105
1056
  e.preventDefault();
1106
1057
  e.stopPropagation();
1107
1058
  };
1108
1059
 
1109
- // Add the event listeners
1110
1060
  canvas.addEventListener("mousedown", this.dom_mousedown_handler);
1111
1061
  window.addEventListener("mousemove", this.dom_mousemove_handler);
1112
1062
  window.addEventListener("mouseup", this.dom_mouseup_handler);
@@ -1144,7 +1094,6 @@ export class ResizeTool implements Tool {
1144
1094
  stage.off("pointerup", this.handle_pointer_up.bind(this));
1145
1095
  stage.off("pointerupoutside", this.handle_pointer_up.bind(this));
1146
1096
 
1147
- // Clean up DOM event listeners
1148
1097
  this.cleanup_dom_event_listeners();
1149
1098
  }
1150
1099
 
@@ -1154,38 +1103,31 @@ export class ResizeTool implements Tool {
1154
1103
  private handle_pointer_move(event: FederatedPointerEvent): void {
1155
1104
  if (this.current_subtool !== "size") return;
1156
1105
 
1157
- // Get the current pointer position in local coordinates
1158
1106
  const local_pos = this.image_editor_context.image_container.toLocal(
1159
1107
  event.global
1160
1108
  );
1161
1109
  const current_position = new Point(local_pos.x, local_pos.y);
1162
1110
 
1163
1111
  if (this.is_moving && this.last_pointer_position) {
1164
- // Handle image dragging
1165
1112
  this.handle_image_dragging(current_position);
1166
1113
 
1167
- // Update the last pointer position for continuous dragging
1168
1114
  this.last_pointer_position = current_position;
1169
1115
  } else if (this.is_dragging && this.selected_handle) {
1170
- // Handle resizing
1171
1116
  if (!this.image_editor_context.background_image) return;
1172
1117
 
1173
1118
  const bg = this.image_editor_context.background_image;
1174
1119
  const maintain_aspect_ratio =
1175
1120
  this.active_corner_index !== -1 && !event.shiftKey;
1176
1121
 
1177
- // Get the original dimensions and position
1178
1122
  const original_width = bg.width;
1179
1123
  const original_height = bg.height;
1180
1124
  const original_x = bg.position.x;
1181
1125
  const original_y = bg.position.y;
1182
1126
  const original_aspect_ratio = original_width / original_height;
1183
1127
 
1184
- // Calculate new dimensions and position based on the active handle
1185
1128
  let dimensions: { width: number; height: number; x: number; y: number };
1186
1129
 
1187
1130
  if (this.active_corner_index !== -1) {
1188
- // Handle corner resizing
1189
1131
  dimensions = this.handle_direct_corner_resize(
1190
1132
  current_position,
1191
1133
  original_width,
@@ -1196,7 +1138,6 @@ export class ResizeTool implements Tool {
1196
1138
  maintain_aspect_ratio
1197
1139
  );
1198
1140
  } else if (this.active_edge_index !== -1) {
1199
- // Handle edge resizing
1200
1141
  dimensions = this.handle_direct_edge_resize(
1201
1142
  current_position,
1202
1143
  original_width,
@@ -1205,10 +1146,9 @@ export class ResizeTool implements Tool {
1205
1146
  original_y
1206
1147
  );
1207
1148
  } else {
1208
- return; // No active handle
1149
+ return;
1209
1150
  }
1210
1151
 
1211
- // Limit dimensions to container size
1212
1152
  dimensions = this.limit_dimensions_to_container(
1213
1153
  dimensions,
1214
1154
  original_x,
@@ -1218,26 +1158,21 @@ export class ResizeTool implements Tool {
1218
1158
  maintain_aspect_ratio
1219
1159
  );
1220
1160
 
1221
- // Apply the new dimensions and position
1222
1161
  bg.width = dimensions.width;
1223
1162
  bg.height = dimensions.height;
1224
1163
  bg.position.set(dimensions.x, dimensions.y);
1225
1164
 
1226
- // Update the resize UI
1227
1165
  this.update_resize_ui();
1228
1166
 
1229
- // Update the last pointer position
1230
1167
  this.last_pointer_position = current_position;
1231
1168
  }
1232
1169
  }
1233
-
1234
1170
  /**
1235
1171
  * handles pointer up events
1236
1172
  */
1237
1173
  private handle_pointer_up(): void {
1238
1174
  if (this.current_subtool !== "size") return;
1239
1175
 
1240
- // Reset all state variables
1241
1176
  this.is_dragging = false;
1242
1177
  this.is_moving = false;
1243
1178
  this.selected_handle = null;
@@ -1245,7 +1180,6 @@ export class ResizeTool implements Tool {
1245
1180
  this.active_corner_index = -1;
1246
1181
  this.active_edge_index = -1;
1247
1182
 
1248
- // Update the resize UI without applying constraints
1249
1183
  this.update_resize_ui();
1250
1184
  this.notify("change");
1251
1185
  }
@@ -1263,58 +1197,46 @@ export class ResizeTool implements Tool {
1263
1197
  return;
1264
1198
  }
1265
1199
 
1266
- // Get the background image and container
1267
1200
  const bg = this.image_editor_context.background_image;
1268
1201
  const image_container =
1269
1202
  this.image_editor_context.image_container.getLocalBounds();
1270
1203
 
1271
- // Calculate the delta in local coordinates
1272
1204
  const dx = current_position.x - this.last_pointer_position.x;
1273
1205
  const dy = current_position.y - this.last_pointer_position.y;
1274
1206
 
1275
- // Update the position directly based on the current position of the background image
1276
1207
  let new_x = bg.position.x + dx;
1277
1208
  let new_y = bg.position.y + dy;
1278
1209
 
1279
- // Apply boundary constraints directly during dragging
1280
1210
  const container_width = image_container.width;
1281
1211
  const container_height = image_container.height;
1282
1212
  const bg_width = bg.width;
1283
1213
  const bg_height = bg.height;
1284
1214
 
1285
- // Calculate effective dimensions accounting for border region
1286
1215
  const effectiveWidth = container_width - this.borderRegion * 2;
1287
1216
  const effectiveHeight = container_height - this.borderRegion * 2;
1288
1217
 
1289
- // For images larger than the effective container area, constrain to maintain border
1290
1218
  if (bg_width > effectiveWidth) {
1291
- // Calculate the minimum and maximum allowed positions
1292
1219
  const min_x = -(bg_width - effectiveWidth) + this.borderRegion;
1293
1220
  const max_x = this.borderRegion;
1294
1221
  new_x = Math.max(min_x, Math.min(max_x, new_x));
1295
1222
  } else {
1296
- // For smaller images, ensure they maintain the border region
1297
1223
  const min_x = this.borderRegion;
1298
1224
  const max_x = container_width - bg_width - this.borderRegion;
1299
1225
  new_x = Math.max(min_x, Math.min(max_x, new_x));
1300
1226
  }
1301
1227
 
1302
1228
  if (bg_height > effectiveHeight) {
1303
- // Calculate the minimum and maximum allowed positions
1304
1229
  const min_y = -(bg_height - effectiveHeight) + this.borderRegion;
1305
1230
  const max_y = this.borderRegion;
1306
1231
  new_y = Math.max(min_y, Math.min(max_y, new_y));
1307
1232
  } else {
1308
- // For smaller images, ensure they maintain the border region
1309
1233
  const min_y = this.borderRegion;
1310
1234
  const max_y = container_height - bg_height - this.borderRegion;
1311
1235
  new_y = Math.max(min_y, Math.min(max_y, new_y));
1312
1236
  }
1313
1237
 
1314
- // Apply the new position
1315
1238
  bg.position.set(new_x, new_y);
1316
1239
 
1317
- // Update the resize UI to match the new position
1318
1240
  this.update_resize_ui();
1319
1241
  }
1320
1242
 
@@ -1339,76 +1261,63 @@ export class ResizeTool implements Tool {
1339
1261
  original_aspect_ratio: number,
1340
1262
  maintain_aspect_ratio: boolean
1341
1263
  ): { width: number; height: number; x: number; y: number } {
1342
- // Calculate the raw dimensions based on mouse position
1343
1264
  let raw_width = 0;
1344
1265
  let raw_height = 0;
1345
1266
  let new_x = original_x;
1346
1267
  let new_y = original_y;
1347
1268
 
1348
- // Calculate raw dimensions based on the active corner
1349
1269
  switch (this.active_corner_index) {
1350
- case 0: // Top-left
1270
+ case 0:
1351
1271
  raw_width = original_x + original_width - current_position.x;
1352
1272
  raw_height = original_y + original_height - current_position.y;
1353
1273
  break;
1354
- case 1: // Top-right
1274
+ case 1:
1355
1275
  raw_width = current_position.x - original_x;
1356
1276
  raw_height = original_y + original_height - current_position.y;
1357
1277
  break;
1358
- case 2: // Bottom-left
1278
+ case 2:
1359
1279
  raw_width = original_x + original_width - current_position.x;
1360
1280
  raw_height = current_position.y - original_y;
1361
1281
  break;
1362
- case 3: // Bottom-right
1282
+ case 3:
1363
1283
  raw_width = current_position.x - original_x;
1364
1284
  raw_height = current_position.y - original_y;
1365
1285
  break;
1366
1286
  }
1367
1287
 
1368
- // Ensure minimum size
1369
1288
  raw_width = Math.max(20, raw_width);
1370
1289
  raw_height = Math.max(20, raw_height);
1371
1290
 
1372
- // Calculate final dimensions
1373
1291
  let new_width = raw_width;
1374
1292
  let new_height = raw_height;
1375
1293
 
1376
- // Apply aspect ratio if needed
1377
1294
  if (maintain_aspect_ratio) {
1378
- // Use a consistent approach to determine which dimension to prioritize
1379
- // This prevents jumping between different calculations
1380
1295
  const diagonal_vector = {
1381
1296
  x: current_position.x - (original_x + original_width / 2),
1382
1297
  y: current_position.y - (original_y + original_height / 2)
1383
1298
  };
1384
1299
 
1385
- // Calculate the angle of movement relative to horizontal
1386
1300
  const angle = Math.abs(Math.atan2(diagonal_vector.y, diagonal_vector.x));
1387
1301
 
1388
- // If moving more horizontally, prioritize width; otherwise, prioritize height
1389
1302
  if (angle < Math.PI / 4 || angle > (3 * Math.PI) / 4) {
1390
- // Prioritize width (horizontal movement)
1391
1303
  new_height = new_width / original_aspect_ratio;
1392
1304
  } else {
1393
- // Prioritize height (vertical movement)
1394
1305
  new_width = new_height * original_aspect_ratio;
1395
1306
  }
1396
1307
  }
1397
1308
 
1398
- // Calculate the new position based on the active corner
1399
1309
  switch (this.active_corner_index) {
1400
- case 0: // Top-left
1310
+ case 0:
1401
1311
  new_x = original_x + original_width - new_width;
1402
1312
  new_y = original_y + original_height - new_height;
1403
1313
  break;
1404
- case 1: // Top-right
1314
+ case 1:
1405
1315
  new_y = original_y + original_height - new_height;
1406
1316
  break;
1407
- case 2: // Bottom-left
1317
+ case 2:
1408
1318
  new_x = original_x + original_width - new_width;
1409
1319
  break;
1410
- case 3: // Bottom-right
1411
- // Position stays the same
1320
+ case 3:
1412
1321
  break;
1413
1322
  }
1414
1323
 
@@ -1432,41 +1341,31 @@ export class ResizeTool implements Tool {
1432
1341
  original_x: number,
1433
1342
  original_y: number
1434
1343
  ): { width: number; height: number; x: number; y: number } {
1435
- // Calculate raw dimensions based on mouse position
1436
1344
  let new_width = original_width;
1437
1345
  let new_height = original_height;
1438
1346
  let new_x = original_x;
1439
1347
  let new_y = original_y;
1440
1348
 
1441
- // Handle edge dragging - directly use mouse position for more intuitive control
1442
1349
  switch (this.active_edge_index) {
1443
- case 0: // Top
1444
- // Calculate height based on distance from bottom edge
1350
+ case 0:
1445
1351
  new_height = Math.max(
1446
1352
  20,
1447
1353
  original_y + original_height - current_position.y
1448
1354
  );
1449
- // Set y position to mouse position
1450
1355
  new_y = current_position.y;
1451
1356
  break;
1452
- case 1: // Bottom
1453
- // Calculate height based on distance from top edge
1357
+ case 1:
1454
1358
  new_height = Math.max(20, current_position.y - original_y);
1455
- // y position stays the same
1456
1359
  break;
1457
- case 2: // Left
1458
- // Calculate width based on distance from right edge
1360
+ case 2:
1459
1361
  new_width = Math.max(
1460
1362
  20,
1461
1363
  original_x + original_width - current_position.x
1462
1364
  );
1463
- // Set x position to mouse position
1464
1365
  new_x = current_position.x;
1465
1366
  break;
1466
- case 3: // Right
1467
- // Calculate width based on distance from left edge
1367
+ case 3:
1468
1368
  new_width = Math.max(20, current_position.x - original_x);
1469
- // x position stays the same
1470
1369
  break;
1471
1370
  }
1472
1371
 
@@ -1499,17 +1398,14 @@ export class ResizeTool implements Tool {
1499
1398
  y: new_y
1500
1399
  } = dimensions;
1501
1400
 
1502
- // Account for border region when determining maximum dimensions
1503
1401
  const effective_container_width =
1504
1402
  this.dimensions.width - this.borderRegion * 2;
1505
1403
  const effective_container_height =
1506
1404
  this.dimensions.height - this.borderRegion * 2;
1507
1405
 
1508
- // Store the original dimensions before limiting
1509
1406
  const pre_limit_width = new_width;
1510
1407
  const pre_limit_height = new_height;
1511
1408
 
1512
- // Step 1: Apply container size limits
1513
1409
  const size_limited = this.apply_size_limits(
1514
1410
  new_width,
1515
1411
  new_height,
@@ -1522,7 +1418,6 @@ export class ResizeTool implements Tool {
1522
1418
  new_width = size_limited.width;
1523
1419
  new_height = size_limited.height;
1524
1420
 
1525
- // Step 2: Recalculate position based on active handles
1526
1421
  const position_adjusted = this.adjust_position_for_resizing(
1527
1422
  new_width,
1528
1423
  new_height,
@@ -1535,7 +1430,6 @@ export class ResizeTool implements Tool {
1535
1430
  new_x = position_adjusted.x;
1536
1431
  new_y = position_adjusted.y;
1537
1432
 
1538
- // Step 3: Apply border region constraints
1539
1433
  const border_constrained = this.apply_border_constraints(
1540
1434
  new_width,
1541
1435
  new_height,
@@ -1563,36 +1457,30 @@ export class ResizeTool implements Tool {
1563
1457
  let new_width = width;
1564
1458
  let new_height = height;
1565
1459
 
1566
- // Check if we need to limit dimensions
1567
1460
  const needs_width_limit = new_width > max_width;
1568
1461
  const needs_height_limit = new_height > max_height;
1569
1462
 
1570
- // If both dimensions need limiting, choose the more restrictive one
1571
1463
  if (needs_width_limit && needs_height_limit) {
1572
1464
  const width_scale = max_width / new_width;
1573
1465
  const height_scale = max_height / new_height;
1574
1466
 
1575
1467
  if (width_scale < height_scale) {
1576
- // Width is more restrictive
1577
1468
  new_width = max_width;
1578
1469
  if (maintain_aspect_ratio) {
1579
1470
  new_height = new_width / aspect_ratio;
1580
1471
  }
1581
1472
  } else {
1582
- // Height is more restrictive
1583
1473
  new_height = max_height;
1584
1474
  if (maintain_aspect_ratio) {
1585
1475
  new_width = new_height * aspect_ratio;
1586
1476
  }
1587
1477
  }
1588
1478
  } else if (needs_width_limit) {
1589
- // Only width needs limiting
1590
1479
  new_width = max_width;
1591
1480
  if (maintain_aspect_ratio) {
1592
1481
  new_height = new_width / aspect_ratio;
1593
1482
  }
1594
1483
  } else if (needs_height_limit) {
1595
- // Only height needs limiting
1596
1484
  new_height = max_height;
1597
1485
  if (maintain_aspect_ratio) {
1598
1486
  new_width = new_height * aspect_ratio;
@@ -1617,30 +1505,27 @@ export class ResizeTool implements Tool {
1617
1505
  let new_x = original_x;
1618
1506
  let new_y = original_y;
1619
1507
 
1620
- // Recalculate position based on the active handle and the new dimensions
1621
1508
  switch (this.active_corner_index) {
1622
- case 0: // Top-left
1509
+ case 0:
1623
1510
  new_x = original_x + original_width - new_width;
1624
1511
  new_y = original_y + original_height - new_height;
1625
1512
  break;
1626
- case 1: // Top-right
1513
+ case 1:
1627
1514
  new_y = original_y + original_height - new_height;
1628
1515
  break;
1629
- case 2: // Bottom-left
1516
+ case 2:
1630
1517
  new_x = original_x + original_width - new_width;
1631
1518
  break;
1632
- case 3: // Bottom-right
1633
- // Position stays the same
1519
+ case 3:
1634
1520
  break;
1635
1521
  }
1636
1522
 
1637
- // Handle edge cases
1638
1523
  if (this.active_edge_index !== -1) {
1639
1524
  switch (this.active_edge_index) {
1640
- case 0: // Top
1525
+ case 0:
1641
1526
  new_y = original_y + original_height - new_height;
1642
1527
  break;
1643
- case 2: // Left
1528
+ case 2:
1644
1529
  new_x = original_x + original_width - new_width;
1645
1530
  break;
1646
1531
  }
@@ -1661,7 +1546,6 @@ export class ResizeTool implements Tool {
1661
1546
  maintain_aspect_ratio: boolean,
1662
1547
  aspect_ratio: number
1663
1548
  ): { width: number; height: number; x: number; y: number } {
1664
- // First apply left and top constraints
1665
1549
  const top_left_constrained = this.apply_top_left_constraints(
1666
1550
  width,
1667
1551
  height,
@@ -1671,7 +1555,6 @@ export class ResizeTool implements Tool {
1671
1555
  aspect_ratio
1672
1556
  );
1673
1557
 
1674
- // Then apply right and bottom constraints
1675
1558
  const all_constrained = this.apply_bottom_right_constraints(
1676
1559
  top_left_constrained.width,
1677
1560
  top_left_constrained.height,
@@ -1681,7 +1564,6 @@ export class ResizeTool implements Tool {
1681
1564
  aspect_ratio
1682
1565
  );
1683
1566
 
1684
- // Ensure minimum dimensions
1685
1567
  all_constrained.width = Math.max(20, all_constrained.width);
1686
1568
  all_constrained.height = Math.max(20, all_constrained.height);
1687
1569
 
@@ -1705,36 +1587,28 @@ export class ResizeTool implements Tool {
1705
1587
  let new_x = x;
1706
1588
  let new_y = y;
1707
1589
 
1708
- // Apply left border constraint
1709
1590
  if (new_x < this.borderRegion) {
1710
- // Adjust width if needed when left edge hits border
1711
1591
  if (
1712
1592
  this.active_corner_index === 0 ||
1713
1593
  this.active_corner_index === 2 ||
1714
1594
  this.active_edge_index === 2
1715
1595
  ) {
1716
- // If resizing from left side, adjust width to maintain the adjusted position
1717
1596
  new_width -= this.borderRegion - new_x;
1718
1597
  if (maintain_aspect_ratio) {
1719
- // Recalculate height based on new width
1720
1598
  new_height = new_width / aspect_ratio;
1721
1599
  }
1722
1600
  }
1723
1601
  new_x = this.borderRegion;
1724
1602
  }
1725
1603
 
1726
- // Apply top border constraint
1727
1604
  if (new_y < this.borderRegion) {
1728
- // Adjust height if needed when top edge hits border
1729
1605
  if (
1730
1606
  this.active_corner_index === 0 ||
1731
1607
  this.active_corner_index === 1 ||
1732
1608
  this.active_edge_index === 0
1733
1609
  ) {
1734
- // If resizing from top side, adjust height to maintain the adjusted position
1735
1610
  new_height -= this.borderRegion - new_y;
1736
1611
  if (maintain_aspect_ratio) {
1737
- // Recalculate width based on new height
1738
1612
  new_width = new_height * aspect_ratio;
1739
1613
  }
1740
1614
  }
@@ -1761,9 +1635,7 @@ export class ResizeTool implements Tool {
1761
1635
  let new_x = x;
1762
1636
  let new_y = y;
1763
1637
 
1764
- // Apply right border constraint
1765
1638
  if (new_x + new_width > this.dimensions.width - this.borderRegion) {
1766
- // If resizing from right side, adjust width
1767
1639
  if (
1768
1640
  this.active_corner_index === 1 ||
1769
1641
  this.active_corner_index === 3 ||
@@ -1774,16 +1646,13 @@ export class ResizeTool implements Tool {
1774
1646
  new_height = new_width / aspect_ratio;
1775
1647
  }
1776
1648
  } else {
1777
- // If resizing from left side, adjust position and preserve width if possible
1778
1649
  const right_edge = new_x + new_width;
1779
1650
  const excess = right_edge - (this.dimensions.width - this.borderRegion);
1780
1651
  new_x = Math.max(this.borderRegion, new_x - excess);
1781
1652
  }
1782
1653
  }
1783
1654
 
1784
- // Apply bottom border constraint
1785
1655
  if (new_y + new_height > this.dimensions.height - this.borderRegion) {
1786
- // If resizing from bottom side, adjust height
1787
1656
  if (
1788
1657
  this.active_corner_index === 2 ||
1789
1658
  this.active_corner_index === 3 ||
@@ -1794,7 +1663,6 @@ export class ResizeTool implements Tool {
1794
1663
  new_width = new_height * aspect_ratio;
1795
1664
  }
1796
1665
  } else {
1797
- // If resizing from top side, adjust position and preserve height if possible
1798
1666
  const bottom_edge = new_y + new_height;
1799
1667
  const excess =
1800
1668
  bottom_edge - (this.dimensions.height - this.borderRegion);
@@ -1815,14 +1683,12 @@ export class ResizeTool implements Tool {
1815
1683
  this.update_resize_ui();
1816
1684
  }
1817
1685
 
1818
- // Make sure the background image has a cursor indicating it can be moved
1819
1686
  if (this.image_editor_context.background_image) {
1820
1687
  const bg = this.image_editor_context.background_image;
1821
1688
  this.current_cursor = bg.cursor;
1822
1689
  bg.cursor = "move";
1823
1690
  }
1824
1691
 
1825
- // Set up DOM event listeners
1826
1692
  this.setup_dom_event_listeners();
1827
1693
  }
1828
1694
 
@@ -1835,13 +1701,11 @@ export class ResizeTool implements Tool {
1835
1701
  this.resize_ui_container.visible = false;
1836
1702
  }
1837
1703
 
1838
- // Reset cursor on background image
1839
1704
  if (this.image_editor_context.background_image) {
1840
1705
  const bg = this.image_editor_context.background_image;
1841
1706
  bg.cursor = this.current_cursor;
1842
1707
  }
1843
1708
 
1844
- // Clean up DOM event listeners
1845
1709
  this.cleanup_dom_event_listeners();
1846
1710
  }
1847
1711
 
@@ -1872,18 +1736,15 @@ export class ResizeTool implements Tool {
1872
1736
  public set_border_region(borderRegion: number): void {
1873
1737
  this.borderRegion = borderRegion;
1874
1738
 
1875
- // Store it on the background image if it exists
1876
1739
  if (this.image_editor_context?.background_image) {
1877
1740
  (this.image_editor_context.background_image as any).borderRegion =
1878
1741
  borderRegion;
1879
1742
  }
1880
1743
 
1881
- // Update UI if needed
1882
1744
  if (this.resize_ui_container) {
1883
1745
  this.update_resize_ui();
1884
1746
  }
1885
1747
 
1886
- // Apply constraints if needed (only if we're in resize mode)
1887
1748
  if (
1888
1749
  this.current_subtool === "size" &&
1889
1750
  this.image_editor_context?.background_image