@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.
- package/CHANGELOG.md +23 -0
- package/Example.svelte +1 -1
- package/IMAGE_EDITOR_OVERVIEW.md +224 -0
- package/ImageEditor.stories.svelte +4 -4
- package/Index.svelte +35 -46
- package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +56 -103
- package/dist/Example.svelte.d.ts +1 -1
- package/dist/Index.svelte +29 -33
- package/dist/Index.svelte.d.ts +17 -16
- package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +47 -88
- package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +13 -15
- package/dist/shared/Anchor.svelte +311 -0
- package/dist/shared/Anchor.svelte.d.ts +18 -0
- package/dist/shared/Controls.svelte +162 -23
- package/dist/shared/Controls.svelte.d.ts +7 -2
- package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
- package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
- package/dist/shared/ImageEditor.svelte +546 -289
- package/dist/shared/ImageEditor.svelte.d.ts +51 -56
- package/dist/shared/Layers.svelte +233 -0
- package/dist/shared/Layers.svelte.d.ts +35 -0
- package/dist/shared/Resize.svelte +218 -0
- package/dist/shared/Resize.svelte.d.ts +29 -0
- package/dist/shared/SecondaryToolbar.svelte +54 -0
- package/dist/shared/SecondaryToolbar.svelte.d.ts +35 -0
- package/dist/shared/Toolbar.svelte +337 -0
- package/dist/shared/Toolbar.svelte.d.ts +42 -0
- package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
- package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
- package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
- package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
- package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
- package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
- package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
- package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
- package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
- package/dist/shared/brush/brush-cursor.d.ts +101 -0
- package/dist/shared/brush/brush-cursor.js +327 -0
- package/dist/shared/brush/brush-textures.d.ts +71 -0
- package/dist/shared/brush/brush-textures.js +404 -0
- package/dist/shared/brush/brush-utils.d.ts +32 -0
- package/dist/shared/brush/brush-utils.js +59 -0
- package/dist/shared/brush/brush.d.ts +152 -0
- package/dist/shared/brush/brush.js +396 -0
- package/dist/shared/brush/types.d.ts +37 -0
- package/dist/shared/brush/types.js +1 -0
- package/dist/shared/core/editor.d.ts +252 -0
- package/dist/shared/core/editor.js +1221 -0
- package/dist/shared/crop/crop.d.ts +201 -0
- package/dist/shared/crop/crop.js +727 -0
- package/dist/shared/image/image.d.ts +89 -0
- package/dist/shared/image/image.js +185 -0
- package/dist/shared/resize/resize.d.ts +270 -0
- package/dist/shared/resize/resize.js +1413 -0
- package/dist/shared/types.d.ts +12 -0
- package/dist/shared/utils/pixi.d.ts +16 -57
- package/dist/shared/utils/pixi.js +15 -113
- package/dist/shared/zoom/zoom.d.ts +136 -0
- package/dist/shared/zoom/zoom.js +548 -0
- package/package.json +11 -10
- package/shared/Anchor.svelte +384 -0
- package/shared/Controls.svelte +194 -27
- package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
- package/shared/ImageEditor.svelte +682 -349
- package/shared/Layers.svelte +249 -0
- package/shared/Resize.svelte +239 -0
- package/shared/SecondaryToolbar.svelte +61 -0
- package/shared/Toolbar.svelte +386 -0
- package/shared/brush/BRUSH_TOOL.md +134 -0
- package/shared/brush/BrushOptions.svelte +200 -0
- package/shared/{tools → brush}/BrushSize.svelte +2 -30
- package/shared/{tools → brush}/ColorField.svelte +3 -1
- package/shared/{tools → brush}/ColorPicker.svelte +86 -9
- package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
- package/shared/brush/brush-cursor.ts +408 -0
- package/shared/brush/brush-textures.ts +508 -0
- package/shared/brush/brush-utils.ts +76 -0
- package/shared/brush/brush.ts +506 -0
- package/shared/brush/types.ts +41 -0
- package/shared/core/EDITOR.md +182 -0
- package/shared/core/editor.ts +1730 -0
- package/shared/crop/CROP.md +139 -0
- package/shared/crop/crop.ts +1027 -0
- package/shared/image/IMAGE.md +135 -0
- package/shared/image/image.ts +300 -0
- package/shared/resize/resize.ts +1894 -0
- package/shared/types.ts +14 -0
- package/shared/utils/pixi.ts +19 -246
- package/shared/zoom/zoom.ts +729 -0
- package/dist/shared/layers/Layers.svelte +0 -231
- package/dist/shared/layers/Layers.svelte.d.ts +0 -18
- package/dist/shared/layers/utils.d.ts +0 -73
- package/dist/shared/layers/utils.js +0 -156
- package/dist/shared/tools/Brush.svelte +0 -215
- package/dist/shared/tools/Brush.svelte.d.ts +0 -29
- package/dist/shared/tools/Crop.svelte +0 -155
- package/dist/shared/tools/Crop.svelte.d.ts +0 -16
- package/dist/shared/tools/Cropper.svelte +0 -271
- package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
- package/dist/shared/tools/Handle.svelte +0 -260
- package/dist/shared/tools/Handle.svelte.d.ts +0 -29
- package/dist/shared/tools/Sources.svelte +0 -257
- package/dist/shared/tools/Sources.svelte.d.ts +0 -34
- package/dist/shared/tools/Tools.svelte +0 -102
- package/dist/shared/tools/Tools.svelte.d.ts +0 -48
- package/dist/shared/tools/brush.d.ts +0 -62
- package/dist/shared/tools/brush.js +0 -133
- package/dist/shared/tools/crop.d.ts +0 -15
- package/dist/shared/tools/crop.js +0 -154
- package/dist/shared/tools/index.d.ts +0 -5
- package/dist/shared/tools/index.js +0 -5
- package/dist/shared/tools/sources.d.ts +0 -46
- package/dist/shared/tools/sources.js +0 -121
- package/dist/shared/tools/types.d.ts +0 -1
- package/shared/layers/Layers.svelte +0 -250
- package/shared/layers/utils.ts +0 -294
- package/shared/tools/Brush.svelte +0 -280
- package/shared/tools/BrushOptions.svelte +0 -180
- package/shared/tools/Crop.svelte +0 -189
- package/shared/tools/Cropper.svelte +0 -346
- package/shared/tools/Handle.svelte +0 -289
- package/shared/tools/Sources.svelte +0 -283
- package/shared/tools/Tools.svelte +0 -162
- package/shared/tools/brush.ts +0 -247
- package/shared/tools/crop.ts +0 -213
- package/shared/tools/index.ts +0 -5
- package/shared/tools/sources.ts +0 -195
- package/shared/tools/types.ts +0 -1
- /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
- /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
|
+
}
|