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