@gradio/imageeditor 0.14.5 → 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 +14 -0
  2. package/InteractiveImageEditor.svelte +4 -7
  3. package/dist/InteractiveImageEditor.svelte +4 -3
  4. package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
  5. package/dist/shared/Controls.svelte +32 -1
  6. package/dist/shared/Controls.svelte.d.ts +2 -0
  7. package/dist/shared/ImageEditor.svelte +23 -1
  8. package/dist/shared/ImageEditor.svelte.d.ts +2 -1
  9. package/dist/shared/brush/brush-cursor.js +0 -42
  10. package/dist/shared/brush/brush-textures.d.ts +19 -2
  11. package/dist/shared/brush/brush-textures.js +120 -82
  12. package/dist/shared/brush/brush.js +0 -52
  13. package/dist/shared/{utils → core}/commands.d.ts +19 -58
  14. package/dist/shared/core/commands.js +70 -0
  15. package/dist/shared/core/editor.d.ts +4 -91
  16. package/dist/shared/core/editor.js +18 -708
  17. package/dist/shared/core/layers.d.ts +143 -0
  18. package/dist/shared/core/layers.js +774 -0
  19. package/dist/shared/image/image.d.ts +14 -4
  20. package/dist/shared/image/image.js +91 -18
  21. package/dist/shared/resize/resize.d.ts +31 -0
  22. package/dist/shared/resize/resize.js +127 -290
  23. package/dist/shared/zoom/zoom.js +0 -51
  24. package/package.json +7 -7
  25. package/shared/Controls.svelte +33 -1
  26. package/shared/ImageEditor.svelte +25 -9
  27. package/shared/Toolbar.svelte +1 -4
  28. package/shared/brush/ColorSwatch.svelte +0 -3
  29. package/shared/brush/brush-cursor.ts +0 -42
  30. package/shared/brush/brush-textures.ts +166 -83
  31. package/shared/brush/brush.ts +0 -53
  32. package/shared/core/commands.ts +118 -0
  33. package/shared/core/editor.ts +33 -957
  34. package/shared/core/layers.ts +1068 -0
  35. package/shared/image/image.ts +139 -21
  36. package/shared/resize/resize.ts +160 -299
  37. package/shared/zoom/zoom.ts +0 -51
  38. package/dist/shared/utils/commands.js +0 -73
  39. package/shared/utils/commands.ts +0 -174
@@ -1,6 +1,52 @@
1
1
  import { Container, Graphics, Point, Rectangle, FederatedPointerEvent } from "pixi.js";
2
2
  import {} from "../core/editor";
3
3
  import {} from "../Toolbar.svelte";
4
+ import {} from "../core/commands";
5
+ /**
6
+ * Command that handles resizing the image and supports undo/redo
7
+ */
8
+ export class ResizeCommand {
9
+ context;
10
+ original_width;
11
+ original_height;
12
+ original_x;
13
+ original_y;
14
+ new_width;
15
+ new_height;
16
+ new_x;
17
+ new_y;
18
+ reset_ui;
19
+ name;
20
+ constructor(context, original_state, new_state, reset_ui) {
21
+ this.context = context;
22
+ this.name = "Resize";
23
+ this.original_width = original_state.width;
24
+ this.original_height = original_state.height;
25
+ this.original_x = original_state.x;
26
+ this.original_y = original_state.y;
27
+ this.new_width = new_state.width;
28
+ this.new_height = new_state.height;
29
+ this.new_x = new_state.x;
30
+ this.new_y = new_state.y;
31
+ this.reset_ui = reset_ui;
32
+ }
33
+ async execute() {
34
+ if (!this.context.background_image)
35
+ return;
36
+ this.context.background_image.width = this.new_width;
37
+ this.context.background_image.height = this.new_height;
38
+ this.context.background_image.position.set(this.new_x, this.new_y);
39
+ this.reset_ui();
40
+ }
41
+ async undo() {
42
+ if (!this.context.background_image)
43
+ return;
44
+ this.context.background_image.width = this.original_width;
45
+ this.context.background_image.height = this.original_height;
46
+ this.context.background_image.position.set(this.original_x, this.original_y);
47
+ this.reset_ui();
48
+ }
49
+ }
4
50
  /**
5
51
  * @class ResizeTool
6
52
  * @implements {Tool}
@@ -12,14 +58,11 @@ export class ResizeTool {
12
58
  current_tool = "image";
13
59
  current_subtool = "size";
14
60
  current_cursor = "unset";
15
- // Constants
16
61
  CORNER_SIZE = 25;
17
62
  LINE_THICKNESS = 5;
18
63
  HANDLE_COLOR = 0x000000;
19
64
  HIT_AREA_SIZE = 40;
20
- // Border region for outpainting
21
65
  borderRegion = 0;
22
- // State
23
66
  is_dragging = false;
24
67
  selected_handle = null;
25
68
  active_corner_index = -1;
@@ -38,6 +81,7 @@ export class ResizeTool {
38
81
  dom_mouseup_handler = null;
39
82
  event_callbacks = new Map();
40
83
  last_scale = 1;
84
+ original_state = null;
41
85
  /**
42
86
  * @method setup
43
87
  * @async
@@ -51,39 +95,29 @@ export class ResizeTool {
51
95
  this.image_editor_context = context;
52
96
  this.current_tool = tool;
53
97
  this.current_subtool = subtool;
54
- // Get the border region from context if it exists
55
98
  if (context.background_image) {
56
- // First try to read from a custom property on the background image
57
- // This is important as the position might have been adjusted
58
99
  if (context.background_image.borderRegion !== undefined) {
59
100
  this.borderRegion = context.background_image.borderRegion;
60
101
  }
61
102
  else {
62
- // Fall back to the position-based detection as before
63
- // But add additional checks to make sure it's not just centering
64
103
  const bg = context.background_image;
65
104
  const container = context.image_container;
66
- // Check if the image position seems intentional (not just centered)
67
105
  const isHorizontallyCentered = Math.abs(bg.position.x - (container.width - bg.width) / 2) < 2;
68
106
  const isVerticallyCentered = Math.abs(bg.position.y - (container.height - bg.height) / 2) < 2;
69
107
  if (!isHorizontallyCentered || !isVerticallyCentered) {
70
- // If not centered, this is likely an intentional border
71
108
  this.borderRegion = Math.max(bg.position.x, bg.position.y);
72
- // Store it for future reference
73
109
  context.background_image.borderRegion = this.borderRegion;
74
110
  }
75
111
  }
76
112
  }
77
113
  context.dimensions.subscribe((dimensions) => {
78
114
  this.dimensions = dimensions;
79
- // Force UI update if it exists
80
115
  if (this.resize_ui_container) {
81
116
  this.update_resize_ui();
82
117
  }
83
118
  });
84
119
  context.position.subscribe((position) => {
85
120
  this.position = position;
86
- // Update resize UI immediately when position changes to avoid lag
87
121
  if (this.resize_ui_container &&
88
122
  this.current_tool === "image" &&
89
123
  this.current_subtool === "size") {
@@ -92,17 +126,14 @@ export class ResizeTool {
92
126
  });
93
127
  context.scale.subscribe((scale) => {
94
128
  this.scale = scale;
95
- // Update resize UI immediately when scale changes to avoid lag
96
129
  if (this.resize_ui_container &&
97
130
  this.current_tool === "image" &&
98
131
  this.current_subtool === "size") {
99
132
  this.update_resize_ui();
100
133
  }
101
134
  });
102
- // Initialize resize UI
103
135
  await this.init_resize_ui();
104
136
  this.setup_event_listeners();
105
- // Apply boundary constraints only if the image is outside the container boundaries
106
137
  if (this.current_subtool === "size" &&
107
138
  this.image_editor_context.background_image) {
108
139
  const bg = this.image_editor_context.background_image;
@@ -112,7 +143,6 @@ export class ResizeTool {
112
143
  const container_bounds = container.getLocalBounds();
113
144
  const container_width = container_bounds.width;
114
145
  const container_height = container_bounds.height;
115
- // Check if the image exceeds the container boundaries
116
146
  const min_x = -(bg_width - container_width);
117
147
  const min_y = -(bg_height - container_height);
118
148
  const max_x = 0;
@@ -121,7 +151,6 @@ export class ResizeTool {
121
151
  (bg.position.x < min_x || bg.position.x > max_x);
122
152
  const y_out_of_bounds = bg_height > container_height &&
123
153
  (bg.position.y < min_y || bg.position.y > max_y);
124
- // Only apply constraints if the image is out of bounds
125
154
  if (x_out_of_bounds || y_out_of_bounds) {
126
155
  this.apply_boundary_constraints();
127
156
  }
@@ -136,7 +165,6 @@ export class ResizeTool {
136
165
  if (this.resize_ui_container) {
137
166
  this.image_editor_context.app.stage.removeChild(this.resize_ui_container);
138
167
  }
139
- // Remove event listeners
140
168
  this.cleanup_event_listeners();
141
169
  }
142
170
  /**
@@ -150,17 +178,13 @@ export class ResizeTool {
150
178
  this.current_tool = tool;
151
179
  this.current_subtool = subtool;
152
180
  if (tool === "image" && subtool === "size") {
153
- // Check if there's a stored border region on the background image
154
181
  if (this.image_editor_context.background_image) {
155
182
  const storedBorderRegion = this.image_editor_context.background_image.borderRegion;
156
183
  if (typeof storedBorderRegion === "number" && storedBorderRegion > 0) {
157
- // Use the stored border region
158
184
  this.borderRegion = storedBorderRegion;
159
185
  }
160
186
  }
161
187
  this.show_resize_ui();
162
- // Apply boundary constraints when switching to the resize tool
163
- // but only if the image is outside the container boundaries
164
188
  if (this.image_editor_context.background_image) {
165
189
  const bg = this.image_editor_context.background_image;
166
190
  const container = this.image_editor_context.image_container;
@@ -169,10 +193,8 @@ export class ResizeTool {
169
193
  const container_bounds = container.getLocalBounds();
170
194
  const container_width = container_bounds.width;
171
195
  const container_height = container_bounds.height;
172
- // Apply effective width/height calculations with border region
173
196
  const effectiveWidth = container_width - this.borderRegion * 2;
174
197
  const effectiveHeight = container_height - this.borderRegion * 2;
175
- // Check if the image exceeds the effective container area or intrudes on border
176
198
  const min_x = bg_width > effectiveWidth
177
199
  ? -(bg_width - effectiveWidth) + this.borderRegion
178
200
  : this.borderRegion;
@@ -185,10 +207,8 @@ export class ResizeTool {
185
207
  const max_y = bg_height > effectiveHeight
186
208
  ? this.borderRegion
187
209
  : container_height - bg_height - this.borderRegion;
188
- // Check if any constraints are violated
189
210
  const x_out_of_bounds = bg.position.x < min_x || bg.position.x > max_x;
190
211
  const y_out_of_bounds = bg.position.y < min_y || bg.position.y > max_y;
191
- // Only apply constraints if the image is out of bounds
192
212
  if (x_out_of_bounds || y_out_of_bounds) {
193
213
  this.apply_boundary_constraints();
194
214
  }
@@ -206,16 +226,10 @@ export class ResizeTool {
206
226
  * @returns {Promise<void>}
207
227
  */
208
228
  async init_resize_ui() {
209
- // Create resize UI with scaled dimensions
210
229
  this.resize_ui_container = this.make_resize_ui(this.dimensions.width * this.scale, this.dimensions.height * this.scale);
211
- // Position the resize UI container at the image position
212
230
  this.resize_ui_container.position.set(this.position.x, this.position.y);
213
- // Initially hide the resize UI until explicitly enabled by set_tool
214
231
  this.resize_ui_container.visible = false;
215
232
  this.image_editor_context.app.stage.addChild(this.resize_ui_container);
216
- // Instead of using the ticker for continuous updates, we'll use immediate updates
217
- // on scale and position changes, which we've added in the setup method
218
- // This will eliminate lag during zoom operations
219
233
  }
220
234
  /**
221
235
  * @method make_resize_ui
@@ -229,61 +243,46 @@ export class ResizeTool {
229
243
  const resize_container = new Container();
230
244
  resize_container.eventMode = "static";
231
245
  resize_container.interactiveChildren = true;
232
- // Create solid outline (will be mostly hidden by the dotted outline)
233
246
  const outline = new Graphics().rect(0, 0, width, height).stroke({
234
247
  width: 1,
235
248
  color: 0x000000,
236
249
  alignment: 0,
237
250
  alpha: 0.3
238
251
  });
239
- // Add a second graphics object for the dotted effect
240
252
  const dotted_outline = new Graphics();
241
- // Use pixelLine for crisp rendering
242
253
  dotted_outline.rect(0, 0, width, height).stroke({
243
254
  width: 1,
244
255
  color: 0x000000,
245
256
  alpha: 0.7,
246
257
  pixelLine: true
247
258
  });
248
- // Create dashed line effect by drawing small rectangles along the border
249
259
  const dash_length = 5;
250
260
  const gap_length = 5;
251
261
  const total_length = dash_length + gap_length;
252
- // Draw dashed horizontal lines (top and bottom) - draw fewer dashes for performance
253
262
  for (let x = 0; x < width; x += total_length * 2) {
254
- // Skip every other dash
255
- // Top dash
256
263
  dotted_outline
257
264
  .rect(x, 0, Math.min(dash_length, width - x), 1)
258
265
  .fill(0x000000);
259
- // Bottom dash
260
266
  dotted_outline
261
267
  .rect(x, height - 1, Math.min(dash_length, width - x), 1)
262
268
  .fill(0x000000);
263
269
  }
264
- // Draw dashed vertical lines (left and right) - draw fewer dashes for performance
265
270
  for (let y = 0; y < height; y += total_length * 2) {
266
- // Skip every other dash
267
- // Left dash
268
271
  dotted_outline
269
272
  .rect(0, y, 1, Math.min(dash_length, height - y))
270
273
  .fill(0x000000);
271
- // Right dash
272
274
  dotted_outline
273
275
  .rect(width - 1, y, 1, Math.min(dash_length, height - y))
274
276
  .fill(0x000000);
275
277
  }
276
278
  resize_container.addChild(outline);
277
279
  resize_container.addChild(dotted_outline);
278
- // Create a transparent interactive area for the move cursor
279
280
  const move_area = new Graphics()
280
281
  .rect(0, 0, width, height)
281
- .fill(0xffffff, 0); // Transparent fill
282
+ .fill(0xffffff, 0);
282
283
  move_area.eventMode = "static";
283
- move_area.cursor = "move"; // Set cursor to move
284
- // Add the move area before the handles so it doesn't interfere with handle interactions
284
+ move_area.cursor = "move";
285
285
  resize_container.addChild(move_area);
286
- // Create handles
287
286
  this.create_corner_handles(resize_container, width, height);
288
287
  this.create_edge_handles(resize_container, width, height);
289
288
  return resize_container;
@@ -300,14 +299,12 @@ export class ResizeTool {
300
299
  handle.eventMode = "static";
301
300
  handle.cursor = "pointer";
302
301
  const handle_graphics = new Graphics();
303
- const handle_size = is_edge ? 8 : 10; // Smaller for edge handles
304
- // Create square handles
302
+ const handle_size = is_edge ? 8 : 10;
305
303
  handle_graphics
306
304
  .rect(-handle_size / 2, -handle_size / 2, handle_size, handle_size)
307
- .fill(0xffffff) // White fill
308
- .stroke({ width: 1, color: this.HANDLE_COLOR }); // Black border
305
+ .fill(0xffffff)
306
+ .stroke({ width: 1, color: this.HANDLE_COLOR });
309
307
  handle.addChild(handle_graphics);
310
- // Adjust hit area based on handle type
311
308
  const hit_size = is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE;
312
309
  handle.hitArea = new Rectangle(-hit_size / 2, -hit_size / 2, hit_size, hit_size);
313
310
  return handle;
@@ -322,27 +319,23 @@ export class ResizeTool {
322
319
  * @returns {void}
323
320
  */
324
321
  create_edge_handles(container, width, height) {
325
- // Horizontal handles at top and bottom
326
322
  [-1, 1].forEach((i, index) => {
327
- const handle = this.create_handle(true); // true for edge handle
323
+ const handle = this.create_handle(true);
328
324
  handle.rotation = 0;
329
325
  handle.on("pointerdown", (event) => {
330
- this.handle_pointer_down(event, handle, -1, index); // 0 for top, 1 for bottom
326
+ this.handle_pointer_down(event, handle, -1, index);
331
327
  });
332
- // Center the handle
333
328
  handle.x = width / 2;
334
329
  handle.y = i < 0 ? 0 : height;
335
330
  handle.cursor = "ns-resize";
336
331
  container.addChild(handle);
337
332
  });
338
- // Vertical handles at left and right
339
333
  [-1, 1].forEach((i, index) => {
340
- const handle = this.create_handle(true); // true for edge handle
341
- handle.rotation = 0; // No rotation needed for square handles
334
+ const handle = this.create_handle(true);
335
+ handle.rotation = 0;
342
336
  handle.on("pointerdown", (event) => {
343
- this.handle_pointer_down(event, handle, -1, index + 2); // 2 for left, 3 for right
337
+ this.handle_pointer_down(event, handle, -1, index + 2);
344
338
  });
345
- // Center the handle
346
339
  handle.x = i < 0 ? 0 : width;
347
340
  handle.y = height / 2;
348
341
  handle.cursor = "ew-resize";
@@ -360,13 +353,13 @@ export class ResizeTool {
360
353
  */
361
354
  create_corner_handles(container, width, height) {
362
355
  const corners = [
363
- { x: 0, y: 0, cursor: "nwse-resize" }, // Top-left
364
- { x: width, y: 0, cursor: "nesw-resize" }, // Top-right
365
- { x: 0, y: height, cursor: "nesw-resize" }, // Bottom-left
366
- { x: width, y: height, cursor: "nwse-resize" } // Bottom-right
356
+ { x: 0, y: 0, cursor: "nwse-resize" },
357
+ { x: width, y: 0, cursor: "nesw-resize" },
358
+ { x: 0, y: height, cursor: "nesw-resize" },
359
+ { x: width, y: height, cursor: "nwse-resize" }
367
360
  ];
368
361
  corners.forEach(({ x, y, cursor }, i) => {
369
- const corner = this.create_handle(false); // false for corner handle
362
+ const corner = this.create_handle(false);
370
363
  corner.x = x;
371
364
  corner.y = y;
372
365
  corner.on("pointerdown", (event) => {
@@ -387,7 +380,6 @@ export class ResizeTool {
387
380
  * @returns {void}
388
381
  */
389
382
  handle_pointer_down(event, handle, corner_index, edge_index) {
390
- // Only handle resize events if resize tool is active
391
383
  if (this.current_subtool !== "size")
392
384
  return;
393
385
  event.stopPropagation();
@@ -395,9 +387,17 @@ export class ResizeTool {
395
387
  this.selected_handle = handle;
396
388
  this.active_corner_index = corner_index;
397
389
  this.active_edge_index = edge_index;
398
- // Convert global position to the container's local coordinates.
399
390
  const local_pos = this.image_editor_context.image_container.toLocal(event.global);
400
391
  this.last_pointer_position = new Point(local_pos.x, local_pos.y);
392
+ if (this.image_editor_context.background_image) {
393
+ const bg = this.image_editor_context.background_image;
394
+ this.original_state = {
395
+ width: bg.width,
396
+ height: bg.height,
397
+ x: bg.position.x,
398
+ y: bg.position.y
399
+ };
400
+ }
401
401
  }
402
402
  /**
403
403
  * Maintains aspect ratio during resizing
@@ -408,23 +408,15 @@ export class ResizeTool {
408
408
  * @returns The adjusted dimensions
409
409
  */
410
410
  maintain_aspect_ratio(new_width, new_height, original_aspect_ratio, delta) {
411
- // For corner resizing, use the dimension that the user is actively changing
412
- // This makes the resize follow the mouse cursor more naturally
413
- // Determine which dimension changed more significantly
414
411
  const abs_delta_x = Math.abs(delta.x);
415
412
  const abs_delta_y = Math.abs(delta.y);
416
- // Use a more responsive approach to determine which dimension to prioritize
417
- // This makes the resize follow the mouse cursor more closely
418
413
  if (abs_delta_x > abs_delta_y * 1.2) {
419
- // User is moving more horizontally, so prioritize width
420
414
  new_height = new_width / original_aspect_ratio;
421
415
  }
422
416
  else if (abs_delta_y > abs_delta_x * 1.2) {
423
- // User is moving more vertically, so prioritize height
424
417
  new_width = new_height * original_aspect_ratio;
425
418
  }
426
419
  else {
427
- // User is moving diagonally, use the larger delta for better control
428
420
  if (new_width / original_aspect_ratio > new_height) {
429
421
  new_height = new_width / original_aspect_ratio;
430
422
  }
@@ -442,26 +434,22 @@ export class ResizeTool {
442
434
  * @returns The limited dimensions
443
435
  */
444
436
  limit_dimensions(width, height, maintain_aspect_ratio) {
445
- // Get container dimensions - restrict to container size
446
437
  const max_width = this.dimensions.width;
447
438
  const max_height = this.dimensions.height;
448
439
  let new_width = width;
449
440
  let new_height = height;
450
441
  if (new_width > max_width) {
451
- // If maintaining aspect ratio, adjust height proportionally
452
442
  if (maintain_aspect_ratio && this.active_corner_index !== -1) {
453
443
  new_height = (max_width / new_width) * new_height;
454
444
  }
455
445
  new_width = max_width;
456
446
  }
457
447
  if (new_height > max_height) {
458
- // If maintaining aspect ratio, adjust width proportionally
459
448
  if (maintain_aspect_ratio && this.active_corner_index !== -1) {
460
449
  new_width = (max_height / new_height) * new_width;
461
450
  }
462
451
  new_height = max_height;
463
452
  }
464
- // Ensure minimum size
465
453
  new_width = Math.max(20, new_width);
466
454
  new_height = Math.max(20, new_height);
467
455
  return { width: new_width, height: new_height };
@@ -477,21 +465,16 @@ export class ResizeTool {
477
465
  calculate_position_deltas(old_width, old_height, new_width, new_height) {
478
466
  let delta_x = 0;
479
467
  let delta_y = 0;
480
- // Calculate position deltas based on which handle is being dragged
481
468
  if (this.active_corner_index === 0 || this.active_edge_index === 2) {
482
- // Left side (top-left corner or left edge)
483
469
  delta_x = old_width - new_width;
484
470
  }
485
471
  else if (this.active_corner_index === 2) {
486
- // Bottom-left corner should anchor to top-right
487
- // So we need to adjust both x and y
488
472
  delta_x = old_width - new_width;
489
- delta_y = 0; // No y adjustment to keep it anchored to the top
473
+ delta_y = 0;
490
474
  }
491
475
  if (this.active_corner_index === 0 ||
492
476
  this.active_corner_index === 1 ||
493
477
  this.active_edge_index === 0) {
494
- // Top side (top-left, top-right corners or top edge)
495
478
  delta_y = old_height - new_height;
496
479
  }
497
480
  return { x: delta_x, y: delta_y };
@@ -505,51 +488,39 @@ export class ResizeTool {
505
488
  return;
506
489
  const bg = this.image_editor_context.background_image;
507
490
  const image_container = this.image_editor_context.image_container;
508
- // Get current position
509
491
  let new_x = bg.position.x;
510
492
  let new_y = bg.position.y;
511
- // Apply boundary constraints to keep the image within the container
512
493
  const container_width = image_container.width;
513
494
  const container_height = image_container.height;
514
495
  const bg_width = bg.width;
515
496
  const bg_height = bg.height;
516
- // For images larger than the effective container area (container minus border),
517
- // constrain them to maintain the border region
518
497
  const effectiveWidth = container_width - this.borderRegion * 2;
519
498
  const effectiveHeight = container_height - this.borderRegion * 2;
520
499
  if (bg_width > effectiveWidth) {
521
- // Calculate the minimum and maximum allowed positions
522
500
  const min_x = -(bg_width - effectiveWidth) + this.borderRegion;
523
501
  const max_x = this.borderRegion;
524
502
  new_x = Math.max(min_x, Math.min(max_x, new_x));
525
503
  }
526
504
  else {
527
- // If image is smaller than effective container, enforce at least the border region on each side
528
505
  const min_x = this.borderRegion;
529
506
  const max_x = container_width - bg_width - this.borderRegion;
530
- // Only constrain if the image would intrude on the border region
531
507
  if (new_x < min_x || new_x > max_x) {
532
508
  new_x = Math.max(min_x, Math.min(max_x, new_x));
533
509
  }
534
510
  }
535
511
  if (bg_height > effectiveHeight) {
536
- // Calculate the minimum and maximum allowed positions
537
512
  const min_y = -(bg_height - effectiveHeight) + this.borderRegion;
538
513
  const max_y = this.borderRegion;
539
514
  new_y = Math.max(min_y, Math.min(max_y, new_y));
540
515
  }
541
516
  else {
542
- // If image is smaller than effective container, enforce at least the border region on each side
543
517
  const min_y = this.borderRegion;
544
518
  const max_y = container_height - bg_height - this.borderRegion;
545
- // Only constrain if the image would intrude on the border region
546
519
  if (new_y < min_y || new_y > max_y) {
547
520
  new_y = Math.max(min_y, Math.min(max_y, new_y));
548
521
  }
549
522
  }
550
- // Apply the constrained position
551
523
  bg.position.set(new_x, new_y);
552
- // Update resize UI with container position and scaled sprite position
553
524
  if (this.resize_ui_container) {
554
525
  const total_x = image_container.position.x + new_x * this.scale;
555
526
  const total_y = image_container.position.y + new_y * this.scale;
@@ -562,71 +533,59 @@ export class ResizeTool {
562
533
  * @param maintain_aspect_ratio - Whether to maintain the aspect ratio
563
534
  */
564
535
  update_resize_bounds(delta, maintain_aspect_ratio = false) {
565
- // The delta is already in local coordinates, no need to scale it further
566
536
  const background_image = this.image_editor_context.background_image;
567
537
  if (!background_image)
568
538
  return;
569
- // Store original dimensions and position
570
539
  const original_width = background_image.width;
571
540
  const original_height = background_image.height;
572
541
  const original_x = background_image.position.x;
573
542
  const original_y = background_image.position.y;
574
543
  const original_aspect_ratio = original_width / original_height;
575
- // Calculate current position based on the delta and the active handle
576
544
  let current_position;
577
545
  if (this.active_corner_index !== -1) {
578
- // For corners, calculate the current position based on the corner and delta
579
546
  switch (this.active_corner_index) {
580
- case 0: // Top-left
547
+ case 0:
581
548
  current_position = new Point(original_x - delta.x, original_y - delta.y);
582
549
  break;
583
- case 1: // Top-right
550
+ case 1:
584
551
  current_position = new Point(original_x + original_width + delta.x, original_y - delta.y);
585
552
  break;
586
- case 2: // Bottom-left
553
+ case 2:
587
554
  current_position = new Point(original_x - delta.x, original_y + original_height + delta.y);
588
555
  break;
589
- case 3: // Bottom-right
556
+ case 3:
590
557
  current_position = new Point(original_x + original_width + delta.x, original_y + original_height + delta.y);
591
558
  break;
592
559
  default:
593
560
  return;
594
561
  }
595
- // Use the existing corner resize logic
596
562
  const dimensions = this.handle_direct_corner_resize(current_position, original_width, original_height, original_x, original_y, original_aspect_ratio, maintain_aspect_ratio);
597
- // Apply the new dimensions and position
598
563
  background_image.width = dimensions.width;
599
564
  background_image.height = dimensions.height;
600
565
  background_image.position.set(dimensions.x, dimensions.y);
601
566
  }
602
567
  else if (this.active_edge_index !== -1) {
603
- // For edges, calculate the current position based on the edge and delta
604
568
  switch (this.active_edge_index) {
605
- case 0: // Top
569
+ case 0:
606
570
  current_position = new Point(original_x + original_width / 2, original_y - delta.y);
607
571
  break;
608
- case 1: // Bottom
572
+ case 1:
609
573
  current_position = new Point(original_x + original_width / 2, original_y + original_height + delta.y);
610
574
  break;
611
- case 2: // Left
575
+ case 2:
612
576
  current_position = new Point(original_x - delta.x, original_y + original_height / 2);
613
577
  break;
614
- case 3: // Right
578
+ case 3:
615
579
  current_position = new Point(original_x + original_width + delta.x, original_y + original_height / 2);
616
580
  break;
617
581
  default:
618
582
  return;
619
583
  }
620
- // Use the existing edge resize logic
621
584
  const dimensions = this.handle_direct_edge_resize(current_position, original_width, original_height, original_x, original_y);
622
- // Apply the new dimensions and position
623
585
  background_image.width = dimensions.width;
624
586
  background_image.height = dimensions.height;
625
587
  background_image.position.set(dimensions.x, dimensions.y);
626
588
  }
627
- // We don't apply boundary constraints during resizing
628
- // This will be done when the user releases the mouse in handle_pointer_up
629
- // Update the resize UI to match the new position and dimensions
630
589
  this.update_resize_ui();
631
590
  }
632
591
  /**
@@ -639,73 +598,51 @@ export class ResizeTool {
639
598
  return;
640
599
  const background_image = this.image_editor_context.background_image;
641
600
  const image_container = this.image_editor_context.image_container;
642
- // Get current image container global position
643
601
  const container_global_pos = image_container.getGlobalPosition();
644
- // Calculate background image position in global coordinates
645
602
  const bg_pos_x = background_image.position.x * this.scale;
646
603
  const bg_pos_y = background_image.position.y * this.scale;
647
- // Position resize UI precisely to match the image position in global space
648
604
  this.resize_ui_container.position.set(container_global_pos.x + bg_pos_x, container_global_pos.y + bg_pos_y);
649
- // Get the scaled dimensions
650
605
  const scaled_width = background_image.width * this.scale;
651
606
  const scaled_height = background_image.height * this.scale;
652
- // Redraw the outline so it matches the resize_bounds size
653
607
  const outline = this.resize_ui_container.getChildAt(0);
654
608
  outline
655
609
  .clear()
656
610
  .rect(0, 0, scaled_width, scaled_height)
657
611
  .stroke({ width: 1, color: 0x000000, alignment: 0, alpha: 0.3 });
658
- // Redraw the dotted outline
659
612
  const dotted_outline = this.resize_ui_container.getChildAt(1);
660
613
  dotted_outline.clear();
661
- // Use simpler more performant approach for the dotted outline
662
- // Just draw the solid outline with pixelLine for crisp rendering
663
614
  dotted_outline.rect(0, 0, scaled_width, scaled_height).stroke({
664
615
  width: 1,
665
616
  color: 0x000000,
666
617
  alpha: 0.7,
667
618
  pixelLine: true
668
619
  });
669
- // Only draw dashes when not actively resizing or moving to improve performance
670
- // And disable dashes during zoom operations to prevent lag
671
620
  if (!this.is_dragging &&
672
621
  !this.is_moving &&
673
622
  Math.abs(this.scale - this.last_scale) < 0.001) {
674
- // Create dashed line effect by drawing small rectangles along the border
675
623
  const dash_length = 5;
676
624
  const gap_length = 5;
677
625
  const total_length = dash_length + gap_length;
678
- // Draw dashed horizontal lines (top and bottom) - draw fewer dashes for performance
679
626
  for (let x = 0; x < scaled_width; x += total_length * 2) {
680
- // Skip every other dash
681
- // Top dash
682
627
  dotted_outline
683
628
  .rect(x, 0, Math.min(dash_length, scaled_width - x), 1)
684
629
  .fill(0x000000);
685
- // Bottom dash
686
630
  dotted_outline
687
631
  .rect(x, scaled_height - 1, Math.min(dash_length, scaled_width - x), 1)
688
632
  .fill(0x000000);
689
633
  }
690
- // Draw dashed vertical lines (left and right) - draw fewer dashes for performance
691
634
  for (let y = 0; y < scaled_height; y += total_length * 2) {
692
- // Skip every other dash
693
- // Left dash
694
635
  dotted_outline
695
636
  .rect(0, y, 1, Math.min(dash_length, scaled_height - y))
696
637
  .fill(0x000000);
697
- // Right dash
698
638
  dotted_outline
699
639
  .rect(scaled_width - 1, y, 1, Math.min(dash_length, scaled_height - y))
700
640
  .fill(0x000000);
701
641
  }
702
642
  }
703
- // Store the current scale for comparison on next update
704
643
  this.last_scale = this.scale;
705
- // Update the move area
706
644
  const move_area = this.resize_ui_container.getChildAt(2);
707
645
  move_area.clear().rect(0, 0, scaled_width, scaled_height).fill(0xffffff, 0);
708
- // Update handle positions to reflect the resized area
709
646
  this.update_handle_positions(scaled_width, scaled_height);
710
647
  }
711
648
  /**
@@ -716,31 +653,25 @@ export class ResizeTool {
716
653
  update_handle_positions(width, height) {
717
654
  if (!this.resize_ui_container)
718
655
  return;
719
- // Skip the outline, dotted outline, and move area (index 0, 1, and 2), get corner/edge handles
720
656
  const handles = this.resize_ui_container.children.slice(3);
721
- // Update the move area size
722
657
  const move_area = this.resize_ui_container.getChildAt(2);
723
658
  move_area.clear().rect(0, 0, width, height).fill(0xffffff, 0);
724
- // First 4 are corners
725
659
  const corners = handles.slice(0, 4);
726
660
  const cornerPositions = [
727
- { x: 0, y: 0 }, // Top-left
728
- { x: width, y: 0 }, // Top-right
729
- { x: 0, y: height }, // Bottom-left
730
- { x: width, y: height } // Bottom-right
661
+ { x: 0, y: 0 },
662
+ { x: width, y: 0 },
663
+ { x: 0, y: height },
664
+ { x: width, y: height }
731
665
  ];
732
666
  corners.forEach((handle, i) => {
733
667
  handle.position.set(cornerPositions[i].x, cornerPositions[i].y);
734
668
  });
735
- // Remaining 4 are edges
736
669
  const edges = handles.slice(4);
737
670
  edges.forEach((handle, i) => {
738
671
  if (i < 2) {
739
- // Top/Bottom edges
740
672
  handle.position.set(width / 2, i === 0 ? 0 : height);
741
673
  }
742
674
  else {
743
- // Left/Right edges
744
675
  handle.position.set(i === 2 ? 0 : width, height / 2);
745
676
  }
746
677
  });
@@ -754,96 +685,90 @@ export class ResizeTool {
754
685
  stage.on("pointermove", this.handle_pointer_move.bind(this));
755
686
  stage.on("pointerup", this.handle_pointer_up.bind(this));
756
687
  stage.on("pointerupoutside", this.handle_pointer_up.bind(this));
757
- // Set up direct DOM event listeners for dragging the background image
758
688
  this.setup_dom_event_listeners();
759
689
  }
760
690
  /**
761
691
  * Sets up direct DOM event listeners for dragging the background image
762
692
  */
763
693
  setup_dom_event_listeners() {
764
- // Clean up any existing DOM event listeners
765
694
  this.cleanup_dom_event_listeners();
766
695
  const canvas = this.image_editor_context.app.canvas;
767
- // Define the mousedown handler
768
696
  this.dom_mousedown_handler = (e) => {
769
- // Only process if we're in the size subtool
770
697
  if (this.current_subtool !== "size")
771
698
  return;
772
- // Check if we're already dragging a handle (resize operation)
773
699
  if (this.is_dragging || this.selected_handle)
774
700
  return;
775
- // Check if we have a background image
776
701
  if (!this.image_editor_context.background_image)
777
702
  return;
778
703
  const bg = this.image_editor_context.background_image;
779
- // Get container and background image bounds
780
704
  const container = this.image_editor_context.image_container;
781
705
  const containerBounds = container.getBounds();
782
- // Get mouse position relative to the canvas
783
706
  const rect = canvas.getBoundingClientRect();
784
707
  const x = e.clientX - rect.left;
785
708
  const y = e.clientY - rect.top;
786
- // Convert to local coordinates within the container
787
709
  const localX = (x - containerBounds.x) / this.scale;
788
710
  const localY = (y - containerBounds.y) / this.scale;
789
- // Check if the click is within the background image bounds using local coordinates
790
711
  if (localX >= bg.position.x &&
791
712
  localX <= bg.position.x + bg.width &&
792
713
  localY >= bg.position.y &&
793
714
  localY <= bg.position.y + bg.height) {
794
- // Set the moving flag to true to indicate we're dragging the image
795
715
  this.is_moving = true;
796
- // Store the initial pointer position in local coordinates
797
716
  this.last_pointer_position = new Point(localX, localY);
798
- // Prevent default behavior and stop propagation
717
+ this.original_state = {
718
+ width: bg.width,
719
+ height: bg.height,
720
+ x: bg.position.x,
721
+ y: bg.position.y
722
+ };
799
723
  e.preventDefault();
800
724
  e.stopPropagation();
801
725
  }
802
726
  };
803
- // Define the mousemove handler
804
727
  this.dom_mousemove_handler = (e) => {
805
- // Only process if we're in the size subtool and we're moving the image
806
728
  if (this.current_subtool !== "size" || !this.is_moving)
807
729
  return;
808
- // Check if we have the necessary state
809
730
  if (!this.last_pointer_position ||
810
731
  !this.image_editor_context.background_image)
811
732
  return;
812
- // Get container bounds
813
733
  const container = this.image_editor_context.image_container;
814
734
  const containerBounds = container.getBounds();
815
- // Get mouse position relative to the canvas
816
735
  const rect = canvas.getBoundingClientRect();
817
736
  const x = e.clientX - rect.left;
818
737
  const y = e.clientY - rect.top;
819
- // Convert to local coordinates within the container
820
738
  const localX = (x - containerBounds.x) / this.scale;
821
739
  const localY = (y - containerBounds.y) / this.scale;
822
- // Create a current position point
823
740
  const current_position = new Point(localX, localY);
824
- // Handle image dragging
825
741
  this.handle_image_dragging(current_position);
826
- // Update the last pointer position for continuous dragging
827
742
  this.last_pointer_position = current_position;
828
- // Prevent default behavior and stop propagation
829
743
  e.preventDefault();
830
744
  e.stopPropagation();
831
745
  };
832
- // Define the mouseup handler
833
746
  this.dom_mouseup_handler = (e) => {
834
- // Only process if we're in the size subtool and we're moving the image
835
- if (this.current_subtool !== "size" || !this.is_moving)
747
+ if (this.current_subtool !== "size")
836
748
  return;
837
- // Reset all state variables
749
+ if (this.original_state && this.image_editor_context.background_image) {
750
+ const bg = this.image_editor_context.background_image;
751
+ const new_state = {
752
+ width: bg.width,
753
+ height: bg.height,
754
+ x: bg.position.x,
755
+ y: bg.position.y
756
+ };
757
+ if (this.original_state.width !== new_state.width ||
758
+ this.original_state.height !== new_state.height ||
759
+ this.original_state.x !== new_state.x ||
760
+ this.original_state.y !== new_state.y) {
761
+ const resize_command = new ResizeCommand(this.image_editor_context, this.original_state, new_state, this.update_resize_ui.bind(this));
762
+ this.image_editor_context.execute_command(resize_command);
763
+ }
764
+ this.original_state = null;
765
+ }
838
766
  this.is_moving = false;
839
767
  this.last_pointer_position = null;
840
- // Update the resize UI without applying constraints
841
768
  this.update_resize_ui();
842
- // Prevent default behavior and stop propagation
843
769
  e.preventDefault();
844
770
  e.stopPropagation();
845
771
  };
846
- // Add the event listeners
847
772
  canvas.addEventListener("mousedown", this.dom_mousedown_handler);
848
773
  window.addEventListener("mousemove", this.dom_mousemove_handler);
849
774
  window.addEventListener("mouseup", this.dom_mouseup_handler);
@@ -874,7 +799,6 @@ export class ResizeTool {
874
799
  stage.off("pointermove", this.handle_pointer_move.bind(this));
875
800
  stage.off("pointerup", this.handle_pointer_up.bind(this));
876
801
  stage.off("pointerupoutside", this.handle_pointer_up.bind(this));
877
- // Clean up DOM event listeners
878
802
  this.cleanup_dom_event_listeners();
879
803
  }
880
804
  /**
@@ -883,49 +807,37 @@ export class ResizeTool {
883
807
  handle_pointer_move(event) {
884
808
  if (this.current_subtool !== "size")
885
809
  return;
886
- // Get the current pointer position in local coordinates
887
810
  const local_pos = this.image_editor_context.image_container.toLocal(event.global);
888
811
  const current_position = new Point(local_pos.x, local_pos.y);
889
812
  if (this.is_moving && this.last_pointer_position) {
890
- // Handle image dragging
891
813
  this.handle_image_dragging(current_position);
892
- // Update the last pointer position for continuous dragging
893
814
  this.last_pointer_position = current_position;
894
815
  }
895
816
  else if (this.is_dragging && this.selected_handle) {
896
- // Handle resizing
897
817
  if (!this.image_editor_context.background_image)
898
818
  return;
899
819
  const bg = this.image_editor_context.background_image;
900
820
  const maintain_aspect_ratio = this.active_corner_index !== -1 && !event.shiftKey;
901
- // Get the original dimensions and position
902
821
  const original_width = bg.width;
903
822
  const original_height = bg.height;
904
823
  const original_x = bg.position.x;
905
824
  const original_y = bg.position.y;
906
825
  const original_aspect_ratio = original_width / original_height;
907
- // Calculate new dimensions and position based on the active handle
908
826
  let dimensions;
909
827
  if (this.active_corner_index !== -1) {
910
- // Handle corner resizing
911
828
  dimensions = this.handle_direct_corner_resize(current_position, original_width, original_height, original_x, original_y, original_aspect_ratio, maintain_aspect_ratio);
912
829
  }
913
830
  else if (this.active_edge_index !== -1) {
914
- // Handle edge resizing
915
831
  dimensions = this.handle_direct_edge_resize(current_position, original_width, original_height, original_x, original_y);
916
832
  }
917
833
  else {
918
- return; // No active handle
834
+ return;
919
835
  }
920
- // Limit dimensions to container size
921
836
  dimensions = this.limit_dimensions_to_container(dimensions, original_x, original_y, original_width, original_height, maintain_aspect_ratio);
922
- // Apply the new dimensions and position
923
837
  bg.width = dimensions.width;
924
838
  bg.height = dimensions.height;
925
839
  bg.position.set(dimensions.x, dimensions.y);
926
- // Update the resize UI
927
840
  this.update_resize_ui();
928
- // Update the last pointer position
929
841
  this.last_pointer_position = current_position;
930
842
  }
931
843
  }
@@ -935,14 +847,12 @@ export class ResizeTool {
935
847
  handle_pointer_up() {
936
848
  if (this.current_subtool !== "size")
937
849
  return;
938
- // Reset all state variables
939
850
  this.is_dragging = false;
940
851
  this.is_moving = false;
941
852
  this.selected_handle = null;
942
853
  this.last_pointer_position = null;
943
854
  this.active_corner_index = -1;
944
855
  this.active_edge_index = -1;
945
- // Update the resize UI without applying constraints
946
856
  this.update_resize_ui();
947
857
  this.notify("change");
948
858
  }
@@ -956,51 +866,39 @@ export class ResizeTool {
956
866
  !this.image_editor_context.background_image) {
957
867
  return;
958
868
  }
959
- // Get the background image and container
960
869
  const bg = this.image_editor_context.background_image;
961
870
  const image_container = this.image_editor_context.image_container.getLocalBounds();
962
- // Calculate the delta in local coordinates
963
871
  const dx = current_position.x - this.last_pointer_position.x;
964
872
  const dy = current_position.y - this.last_pointer_position.y;
965
- // Update the position directly based on the current position of the background image
966
873
  let new_x = bg.position.x + dx;
967
874
  let new_y = bg.position.y + dy;
968
- // Apply boundary constraints directly during dragging
969
875
  const container_width = image_container.width;
970
876
  const container_height = image_container.height;
971
877
  const bg_width = bg.width;
972
878
  const bg_height = bg.height;
973
- // Calculate effective dimensions accounting for border region
974
879
  const effectiveWidth = container_width - this.borderRegion * 2;
975
880
  const effectiveHeight = container_height - this.borderRegion * 2;
976
- // For images larger than the effective container area, constrain to maintain border
977
881
  if (bg_width > effectiveWidth) {
978
- // Calculate the minimum and maximum allowed positions
979
882
  const min_x = -(bg_width - effectiveWidth) + this.borderRegion;
980
883
  const max_x = this.borderRegion;
981
884
  new_x = Math.max(min_x, Math.min(max_x, new_x));
982
885
  }
983
886
  else {
984
- // For smaller images, ensure they maintain the border region
985
887
  const min_x = this.borderRegion;
986
888
  const max_x = container_width - bg_width - this.borderRegion;
987
889
  new_x = Math.max(min_x, Math.min(max_x, new_x));
988
890
  }
989
891
  if (bg_height > effectiveHeight) {
990
- // Calculate the minimum and maximum allowed positions
991
892
  const min_y = -(bg_height - effectiveHeight) + this.borderRegion;
992
893
  const max_y = this.borderRegion;
993
894
  new_y = Math.max(min_y, Math.min(max_y, new_y));
994
895
  }
995
896
  else {
996
- // For smaller images, ensure they maintain the border region
997
897
  const min_y = this.borderRegion;
998
898
  const max_y = container_height - bg_height - this.borderRegion;
999
899
  new_y = Math.max(min_y, Math.min(max_y, new_y));
1000
900
  }
1001
- // Apply the new position
1002
901
  bg.position.set(new_x, new_y);
1003
- // Update the resize UI to match the new position
1004
902
  this.update_resize_ui();
1005
903
  }
1006
904
  /**
@@ -1016,70 +914,57 @@ export class ResizeTool {
1016
914
  * @private
1017
915
  */
1018
916
  handle_direct_corner_resize(current_position, original_width, original_height, original_x, original_y, original_aspect_ratio, maintain_aspect_ratio) {
1019
- // Calculate the raw dimensions based on mouse position
1020
917
  let raw_width = 0;
1021
918
  let raw_height = 0;
1022
919
  let new_x = original_x;
1023
920
  let new_y = original_y;
1024
- // Calculate raw dimensions based on the active corner
1025
921
  switch (this.active_corner_index) {
1026
- case 0: // Top-left
922
+ case 0:
1027
923
  raw_width = original_x + original_width - current_position.x;
1028
924
  raw_height = original_y + original_height - current_position.y;
1029
925
  break;
1030
- case 1: // Top-right
926
+ case 1:
1031
927
  raw_width = current_position.x - original_x;
1032
928
  raw_height = original_y + original_height - current_position.y;
1033
929
  break;
1034
- case 2: // Bottom-left
930
+ case 2:
1035
931
  raw_width = original_x + original_width - current_position.x;
1036
932
  raw_height = current_position.y - original_y;
1037
933
  break;
1038
- case 3: // Bottom-right
934
+ case 3:
1039
935
  raw_width = current_position.x - original_x;
1040
936
  raw_height = current_position.y - original_y;
1041
937
  break;
1042
938
  }
1043
- // Ensure minimum size
1044
939
  raw_width = Math.max(20, raw_width);
1045
940
  raw_height = Math.max(20, raw_height);
1046
- // Calculate final dimensions
1047
941
  let new_width = raw_width;
1048
942
  let new_height = raw_height;
1049
- // Apply aspect ratio if needed
1050
943
  if (maintain_aspect_ratio) {
1051
- // Use a consistent approach to determine which dimension to prioritize
1052
- // This prevents jumping between different calculations
1053
944
  const diagonal_vector = {
1054
945
  x: current_position.x - (original_x + original_width / 2),
1055
946
  y: current_position.y - (original_y + original_height / 2)
1056
947
  };
1057
- // Calculate the angle of movement relative to horizontal
1058
948
  const angle = Math.abs(Math.atan2(diagonal_vector.y, diagonal_vector.x));
1059
- // If moving more horizontally, prioritize width; otherwise, prioritize height
1060
949
  if (angle < Math.PI / 4 || angle > (3 * Math.PI) / 4) {
1061
- // Prioritize width (horizontal movement)
1062
950
  new_height = new_width / original_aspect_ratio;
1063
951
  }
1064
952
  else {
1065
- // Prioritize height (vertical movement)
1066
953
  new_width = new_height * original_aspect_ratio;
1067
954
  }
1068
955
  }
1069
- // Calculate the new position based on the active corner
1070
956
  switch (this.active_corner_index) {
1071
- case 0: // Top-left
957
+ case 0:
1072
958
  new_x = original_x + original_width - new_width;
1073
959
  new_y = original_y + original_height - new_height;
1074
960
  break;
1075
- case 1: // Top-right
961
+ case 1:
1076
962
  new_y = original_y + original_height - new_height;
1077
963
  break;
1078
- case 2: // Bottom-left
964
+ case 2:
1079
965
  new_x = original_x + original_width - new_width;
1080
966
  break;
1081
- case 3: // Bottom-right
1082
- // Position stays the same
967
+ case 3:
1083
968
  break;
1084
969
  }
1085
970
  return { width: new_width, height: new_height, x: new_x, y: new_y };
@@ -1095,34 +980,24 @@ export class ResizeTool {
1095
980
  * @private
1096
981
  */
1097
982
  handle_direct_edge_resize(current_position, original_width, original_height, original_x, original_y) {
1098
- // Calculate raw dimensions based on mouse position
1099
983
  let new_width = original_width;
1100
984
  let new_height = original_height;
1101
985
  let new_x = original_x;
1102
986
  let new_y = original_y;
1103
- // Handle edge dragging - directly use mouse position for more intuitive control
1104
987
  switch (this.active_edge_index) {
1105
- case 0: // Top
1106
- // Calculate height based on distance from bottom edge
988
+ case 0:
1107
989
  new_height = Math.max(20, original_y + original_height - current_position.y);
1108
- // Set y position to mouse position
1109
990
  new_y = current_position.y;
1110
991
  break;
1111
- case 1: // Bottom
1112
- // Calculate height based on distance from top edge
992
+ case 1:
1113
993
  new_height = Math.max(20, current_position.y - original_y);
1114
- // y position stays the same
1115
994
  break;
1116
- case 2: // Left
1117
- // Calculate width based on distance from right edge
995
+ case 2:
1118
996
  new_width = Math.max(20, original_x + original_width - current_position.x);
1119
- // Set x position to mouse position
1120
997
  new_x = current_position.x;
1121
998
  break;
1122
- case 3: // Right
1123
- // Calculate width based on distance from left edge
999
+ case 3:
1124
1000
  new_width = Math.max(20, current_position.x - original_x);
1125
- // x position stays the same
1126
1001
  break;
1127
1002
  }
1128
1003
  return { width: new_width, height: new_height, x: new_x, y: new_y };
@@ -1140,21 +1015,16 @@ export class ResizeTool {
1140
1015
  */
1141
1016
  limit_dimensions_to_container(dimensions, original_x, original_y, original_width, original_height, maintain_aspect_ratio) {
1142
1017
  let { width: new_width, height: new_height, x: new_x, y: new_y } = dimensions;
1143
- // Account for border region when determining maximum dimensions
1144
1018
  const effective_container_width = this.dimensions.width - this.borderRegion * 2;
1145
1019
  const effective_container_height = this.dimensions.height - this.borderRegion * 2;
1146
- // Store the original dimensions before limiting
1147
1020
  const pre_limit_width = new_width;
1148
1021
  const pre_limit_height = new_height;
1149
- // Step 1: Apply container size limits
1150
1022
  const size_limited = this.apply_size_limits(new_width, new_height, effective_container_width, effective_container_height, maintain_aspect_ratio, pre_limit_width / pre_limit_height);
1151
1023
  new_width = size_limited.width;
1152
1024
  new_height = size_limited.height;
1153
- // Step 2: Recalculate position based on active handles
1154
1025
  const position_adjusted = this.adjust_position_for_resizing(new_width, new_height, original_x, original_y, original_width, original_height);
1155
1026
  new_x = position_adjusted.x;
1156
1027
  new_y = position_adjusted.y;
1157
- // Step 3: Apply border region constraints
1158
1028
  const border_constrained = this.apply_border_constraints(new_width, new_height, new_x, new_y, maintain_aspect_ratio, pre_limit_width / pre_limit_height);
1159
1029
  return border_constrained;
1160
1030
  }
@@ -1165,22 +1035,18 @@ export class ResizeTool {
1165
1035
  apply_size_limits(width, height, max_width, max_height, maintain_aspect_ratio, aspect_ratio) {
1166
1036
  let new_width = width;
1167
1037
  let new_height = height;
1168
- // Check if we need to limit dimensions
1169
1038
  const needs_width_limit = new_width > max_width;
1170
1039
  const needs_height_limit = new_height > max_height;
1171
- // If both dimensions need limiting, choose the more restrictive one
1172
1040
  if (needs_width_limit && needs_height_limit) {
1173
1041
  const width_scale = max_width / new_width;
1174
1042
  const height_scale = max_height / new_height;
1175
1043
  if (width_scale < height_scale) {
1176
- // Width is more restrictive
1177
1044
  new_width = max_width;
1178
1045
  if (maintain_aspect_ratio) {
1179
1046
  new_height = new_width / aspect_ratio;
1180
1047
  }
1181
1048
  }
1182
1049
  else {
1183
- // Height is more restrictive
1184
1050
  new_height = max_height;
1185
1051
  if (maintain_aspect_ratio) {
1186
1052
  new_width = new_height * aspect_ratio;
@@ -1188,14 +1054,12 @@ export class ResizeTool {
1188
1054
  }
1189
1055
  }
1190
1056
  else if (needs_width_limit) {
1191
- // Only width needs limiting
1192
1057
  new_width = max_width;
1193
1058
  if (maintain_aspect_ratio) {
1194
1059
  new_height = new_width / aspect_ratio;
1195
1060
  }
1196
1061
  }
1197
1062
  else if (needs_height_limit) {
1198
- // Only height needs limiting
1199
1063
  new_height = max_height;
1200
1064
  if (maintain_aspect_ratio) {
1201
1065
  new_width = new_height * aspect_ratio;
@@ -1210,29 +1074,26 @@ export class ResizeTool {
1210
1074
  adjust_position_for_resizing(new_width, new_height, original_x, original_y, original_width, original_height) {
1211
1075
  let new_x = original_x;
1212
1076
  let new_y = original_y;
1213
- // Recalculate position based on the active handle and the new dimensions
1214
1077
  switch (this.active_corner_index) {
1215
- case 0: // Top-left
1078
+ case 0:
1216
1079
  new_x = original_x + original_width - new_width;
1217
1080
  new_y = original_y + original_height - new_height;
1218
1081
  break;
1219
- case 1: // Top-right
1082
+ case 1:
1220
1083
  new_y = original_y + original_height - new_height;
1221
1084
  break;
1222
- case 2: // Bottom-left
1085
+ case 2:
1223
1086
  new_x = original_x + original_width - new_width;
1224
1087
  break;
1225
- case 3: // Bottom-right
1226
- // Position stays the same
1088
+ case 3:
1227
1089
  break;
1228
1090
  }
1229
- // Handle edge cases
1230
1091
  if (this.active_edge_index !== -1) {
1231
1092
  switch (this.active_edge_index) {
1232
- case 0: // Top
1093
+ case 0:
1233
1094
  new_y = original_y + original_height - new_height;
1234
1095
  break;
1235
- case 2: // Left
1096
+ case 2:
1236
1097
  new_x = original_x + original_width - new_width;
1237
1098
  break;
1238
1099
  }
@@ -1244,11 +1105,8 @@ export class ResizeTool {
1244
1105
  * @private
1245
1106
  */
1246
1107
  apply_border_constraints(width, height, x, y, maintain_aspect_ratio, aspect_ratio) {
1247
- // First apply left and top constraints
1248
1108
  const top_left_constrained = this.apply_top_left_constraints(width, height, x, y, maintain_aspect_ratio, aspect_ratio);
1249
- // Then apply right and bottom constraints
1250
1109
  const all_constrained = this.apply_bottom_right_constraints(top_left_constrained.width, top_left_constrained.height, top_left_constrained.x, top_left_constrained.y, maintain_aspect_ratio, aspect_ratio);
1251
- // Ensure minimum dimensions
1252
1110
  all_constrained.width = Math.max(20, all_constrained.width);
1253
1111
  all_constrained.height = Math.max(20, all_constrained.height);
1254
1112
  return all_constrained;
@@ -1262,31 +1120,23 @@ export class ResizeTool {
1262
1120
  let new_height = height;
1263
1121
  let new_x = x;
1264
1122
  let new_y = y;
1265
- // Apply left border constraint
1266
1123
  if (new_x < this.borderRegion) {
1267
- // Adjust width if needed when left edge hits border
1268
1124
  if (this.active_corner_index === 0 ||
1269
1125
  this.active_corner_index === 2 ||
1270
1126
  this.active_edge_index === 2) {
1271
- // If resizing from left side, adjust width to maintain the adjusted position
1272
1127
  new_width -= this.borderRegion - new_x;
1273
1128
  if (maintain_aspect_ratio) {
1274
- // Recalculate height based on new width
1275
1129
  new_height = new_width / aspect_ratio;
1276
1130
  }
1277
1131
  }
1278
1132
  new_x = this.borderRegion;
1279
1133
  }
1280
- // Apply top border constraint
1281
1134
  if (new_y < this.borderRegion) {
1282
- // Adjust height if needed when top edge hits border
1283
1135
  if (this.active_corner_index === 0 ||
1284
1136
  this.active_corner_index === 1 ||
1285
1137
  this.active_edge_index === 0) {
1286
- // If resizing from top side, adjust height to maintain the adjusted position
1287
1138
  new_height -= this.borderRegion - new_y;
1288
1139
  if (maintain_aspect_ratio) {
1289
- // Recalculate width based on new height
1290
1140
  new_width = new_height * aspect_ratio;
1291
1141
  }
1292
1142
  }
@@ -1303,9 +1153,7 @@ export class ResizeTool {
1303
1153
  let new_height = height;
1304
1154
  let new_x = x;
1305
1155
  let new_y = y;
1306
- // Apply right border constraint
1307
1156
  if (new_x + new_width > this.dimensions.width - this.borderRegion) {
1308
- // If resizing from right side, adjust width
1309
1157
  if (this.active_corner_index === 1 ||
1310
1158
  this.active_corner_index === 3 ||
1311
1159
  this.active_edge_index === 3) {
@@ -1315,15 +1163,12 @@ export class ResizeTool {
1315
1163
  }
1316
1164
  }
1317
1165
  else {
1318
- // If resizing from left side, adjust position and preserve width if possible
1319
1166
  const right_edge = new_x + new_width;
1320
1167
  const excess = right_edge - (this.dimensions.width - this.borderRegion);
1321
1168
  new_x = Math.max(this.borderRegion, new_x - excess);
1322
1169
  }
1323
1170
  }
1324
- // Apply bottom border constraint
1325
1171
  if (new_y + new_height > this.dimensions.height - this.borderRegion) {
1326
- // If resizing from bottom side, adjust height
1327
1172
  if (this.active_corner_index === 2 ||
1328
1173
  this.active_corner_index === 3 ||
1329
1174
  this.active_edge_index === 1) {
@@ -1333,7 +1178,6 @@ export class ResizeTool {
1333
1178
  }
1334
1179
  }
1335
1180
  else {
1336
- // If resizing from top side, adjust position and preserve height if possible
1337
1181
  const bottom_edge = new_y + new_height;
1338
1182
  const excess = bottom_edge - (this.dimensions.height - this.borderRegion);
1339
1183
  new_y = Math.max(this.borderRegion, new_y - excess);
@@ -1350,13 +1194,11 @@ export class ResizeTool {
1350
1194
  this.resize_ui_container.visible = true;
1351
1195
  this.update_resize_ui();
1352
1196
  }
1353
- // Make sure the background image has a cursor indicating it can be moved
1354
1197
  if (this.image_editor_context.background_image) {
1355
1198
  const bg = this.image_editor_context.background_image;
1356
1199
  this.current_cursor = bg.cursor;
1357
1200
  bg.cursor = "move";
1358
1201
  }
1359
- // Set up DOM event listeners
1360
1202
  this.setup_dom_event_listeners();
1361
1203
  }
1362
1204
  /**
@@ -1367,12 +1209,10 @@ export class ResizeTool {
1367
1209
  if (this.resize_ui_container) {
1368
1210
  this.resize_ui_container.visible = false;
1369
1211
  }
1370
- // Reset cursor on background image
1371
1212
  if (this.image_editor_context.background_image) {
1372
1213
  const bg = this.image_editor_context.background_image;
1373
1214
  bg.cursor = this.current_cursor;
1374
1215
  }
1375
- // Clean up DOM event listeners
1376
1216
  this.cleanup_dom_event_listeners();
1377
1217
  }
1378
1218
  on(event, callback) {
@@ -1395,16 +1235,13 @@ export class ResizeTool {
1395
1235
  */
1396
1236
  set_border_region(borderRegion) {
1397
1237
  this.borderRegion = borderRegion;
1398
- // Store it on the background image if it exists
1399
1238
  if (this.image_editor_context?.background_image) {
1400
1239
  this.image_editor_context.background_image.borderRegion =
1401
1240
  borderRegion;
1402
1241
  }
1403
- // Update UI if needed
1404
1242
  if (this.resize_ui_container) {
1405
1243
  this.update_resize_ui();
1406
1244
  }
1407
- // Apply constraints if needed (only if we're in resize mode)
1408
1245
  if (this.current_subtool === "size" &&
1409
1246
  this.image_editor_context?.background_image) {
1410
1247
  this.apply_boundary_constraints();