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