@elixpo/lixsketch 4.5.8
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/LICENSE +21 -0
- package/README.md +169 -0
- package/fonts/fonts.css +29 -0
- package/fonts/lixCode.ttf +0 -0
- package/fonts/lixDefault.ttf +0 -0
- package/fonts/lixDocs.ttf +0 -0
- package/fonts/lixFancy.ttf +0 -0
- package/fonts/lixFont.woff2 +0 -0
- package/package.json +49 -0
- package/src/SketchEngine.js +473 -0
- package/src/core/AIRenderer.js +1390 -0
- package/src/core/CopyPaste.js +655 -0
- package/src/core/EraserTrail.js +234 -0
- package/src/core/EventDispatcher.js +371 -0
- package/src/core/GraphEngine.js +150 -0
- package/src/core/GraphMathParser.js +231 -0
- package/src/core/GraphRenderer.js +255 -0
- package/src/core/LayerOrder.js +91 -0
- package/src/core/LixScriptParser.js +1299 -0
- package/src/core/MermaidFlowchartRenderer.js +475 -0
- package/src/core/MermaidSequenceParser.js +197 -0
- package/src/core/MermaidSequenceRenderer.js +479 -0
- package/src/core/ResizeCode.js +175 -0
- package/src/core/ResizeShapes.js +318 -0
- package/src/core/SceneSerializer.js +778 -0
- package/src/core/Selection.js +1861 -0
- package/src/core/SnapGuides.js +273 -0
- package/src/core/UndoRedo.js +1358 -0
- package/src/core/ZoomPan.js +258 -0
- package/src/core/ai-system-prompt.js +663 -0
- package/src/index.js +69 -0
- package/src/shapes/Arrow.js +1979 -0
- package/src/shapes/Circle.js +751 -0
- package/src/shapes/CodeShape.js +244 -0
- package/src/shapes/Frame.js +1460 -0
- package/src/shapes/FreehandStroke.js +724 -0
- package/src/shapes/IconShape.js +265 -0
- package/src/shapes/ImageShape.js +270 -0
- package/src/shapes/Line.js +738 -0
- package/src/shapes/Rectangle.js +794 -0
- package/src/shapes/TextShape.js +225 -0
- package/src/tools/arrowTool.js +581 -0
- package/src/tools/circleTool.js +619 -0
- package/src/tools/codeTool.js +2103 -0
- package/src/tools/eraserTool.js +131 -0
- package/src/tools/frameTool.js +241 -0
- package/src/tools/freehandTool.js +620 -0
- package/src/tools/iconTool.js +1344 -0
- package/src/tools/imageTool.js +1323 -0
- package/src/tools/laserTool.js +317 -0
- package/src/tools/lineTool.js +502 -0
- package/src/tools/rectangleTool.js +544 -0
- package/src/tools/textTool.js +1823 -0
- package/src/utils/imageCompressor.js +107 -0
|
@@ -0,0 +1,1323 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Image tool event handlers - extracted from imageTool.js
|
|
3
|
+
import { pushCreateAction, pushDeleteAction, pushTransformAction, pushFrameAttachmentAction } from '../core/UndoRedo.js';
|
|
4
|
+
import { updateAttachedArrows as updateArrowsForShape, cleanupAttachments } from './arrowTool.js';
|
|
5
|
+
import { compressImage } from '../utils/imageCompressor.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
let isDraggingImage = false;
|
|
9
|
+
let imageToPlace = null;
|
|
10
|
+
let imageX = 0;
|
|
11
|
+
let imageY = 0;
|
|
12
|
+
let scaleFactor = 0.2;
|
|
13
|
+
let currentImageElement = null;
|
|
14
|
+
|
|
15
|
+
let selectedImage = null;
|
|
16
|
+
let originalX, originalY, originalWidth, originalHeight;
|
|
17
|
+
let currentAnchor = null;
|
|
18
|
+
let isDragging = false;
|
|
19
|
+
let isRotatingImage = false;
|
|
20
|
+
let dragOffsetX, dragOffsetY;
|
|
21
|
+
let startRotationMouseAngle = null;
|
|
22
|
+
let startImageRotation = null;
|
|
23
|
+
let imageRotation = 0;
|
|
24
|
+
let aspect_ratio_lock = true;
|
|
25
|
+
const minImageSize = 20;
|
|
26
|
+
|
|
27
|
+
// Frame attachment variables
|
|
28
|
+
let draggedShapeInitialFrameImage = null;
|
|
29
|
+
let hoveredFrameImage = null;
|
|
30
|
+
|
|
31
|
+
// Per-room image size limit: 5MB total
|
|
32
|
+
const ROOM_IMAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
33
|
+
if (!window.__roomImageBytesUsed) window.__roomImageBytesUsed = 0;
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// Convert SVG element to our ImageShape class
|
|
37
|
+
function wrapImageElement(element) {
|
|
38
|
+
const imageShape = new ImageShape(element);
|
|
39
|
+
return imageShape;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Async image upload pipeline:
|
|
44
|
+
* 1. Show loading indicator on the image
|
|
45
|
+
* 2. Compress the image adaptively
|
|
46
|
+
* 3. Get a signed upload URL from the worker
|
|
47
|
+
* 4. Upload compressed image to Cloudinary
|
|
48
|
+
* 5. Replace the base64 href with Cloudinary URL
|
|
49
|
+
* 6. Remove loading indicator
|
|
50
|
+
*/
|
|
51
|
+
async function uploadImageToCloudinary(imageShape) {
|
|
52
|
+
const workerUrl = window.__WORKER_URL;
|
|
53
|
+
const sessionId = window.__sessionID;
|
|
54
|
+
if (!workerUrl || !sessionId) return;
|
|
55
|
+
|
|
56
|
+
const href = imageShape.element.getAttribute('href') || '';
|
|
57
|
+
if (!href.startsWith('data:')) return; // already a URL, skip
|
|
58
|
+
|
|
59
|
+
imageShape.uploadStatus = 'uploading';
|
|
60
|
+
imageShape.uploadAbortController = new AbortController();
|
|
61
|
+
const signal = imageShape.uploadAbortController.signal;
|
|
62
|
+
|
|
63
|
+
imageShape.showUploadIndicator();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Step 1: Compress
|
|
67
|
+
const compressed = await compressImage(href);
|
|
68
|
+
if (signal.aborted) return;
|
|
69
|
+
|
|
70
|
+
// Step 2: Get signed upload params
|
|
71
|
+
const signRes = await fetch(`${workerUrl}/api/images/sign`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
sessionId,
|
|
76
|
+
filename: `img_${Date.now()}`,
|
|
77
|
+
}),
|
|
78
|
+
signal,
|
|
79
|
+
});
|
|
80
|
+
if (!signRes.ok) throw new Error('Failed to get upload signature');
|
|
81
|
+
const signData = await signRes.json();
|
|
82
|
+
if (signal.aborted) return;
|
|
83
|
+
|
|
84
|
+
// Step 3: Upload to Cloudinary
|
|
85
|
+
const formData = new FormData();
|
|
86
|
+
formData.append('file', compressed.blob);
|
|
87
|
+
formData.append('api_key', signData.apiKey);
|
|
88
|
+
formData.append('timestamp', String(signData.timestamp));
|
|
89
|
+
formData.append('signature', signData.signature);
|
|
90
|
+
formData.append('folder', signData.folder);
|
|
91
|
+
formData.append('public_id', signData.publicId);
|
|
92
|
+
|
|
93
|
+
const uploadRes = await fetch(
|
|
94
|
+
`https://api.cloudinary.com/v1_1/${signData.cloudName}/image/upload`,
|
|
95
|
+
{ method: 'POST', body: formData, signal }
|
|
96
|
+
);
|
|
97
|
+
if (!uploadRes.ok) throw new Error('Cloudinary upload failed');
|
|
98
|
+
const uploadData = await uploadRes.json();
|
|
99
|
+
if (signal.aborted) return;
|
|
100
|
+
|
|
101
|
+
// Step 4: Replace base64 with Cloudinary URL
|
|
102
|
+
const cloudUrl = uploadData.secure_url || uploadData.url;
|
|
103
|
+
imageShape.element.setAttribute('href', cloudUrl);
|
|
104
|
+
imageShape.element.setAttribute('data-href', cloudUrl);
|
|
105
|
+
imageShape.element.setAttribute('data-cloudinary-id', uploadData.public_id);
|
|
106
|
+
|
|
107
|
+
// Update file size tracking with actual compressed size
|
|
108
|
+
const oldSize = imageShape.element.__fileSize || 0;
|
|
109
|
+
const newSize = uploadData.bytes || compressed.compressedSize;
|
|
110
|
+
imageShape.element.__fileSize = newSize;
|
|
111
|
+
window.__roomImageBytesUsed = Math.max(0, (window.__roomImageBytesUsed || 0) - oldSize + newSize);
|
|
112
|
+
|
|
113
|
+
imageShape.uploadStatus = 'done';
|
|
114
|
+
console.log(`[ImageUpload] Uploaded to Cloudinary: ${cloudUrl} (${(newSize / 1024).toFixed(1)}KB)`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (signal.aborted) {
|
|
117
|
+
console.log('[ImageUpload] Upload aborted (image deleted)');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.warn('[ImageUpload] Upload failed:', err);
|
|
121
|
+
imageShape.uploadStatus = 'failed';
|
|
122
|
+
} finally {
|
|
123
|
+
imageShape.removeUploadIndicator();
|
|
124
|
+
imageShape.uploadAbortController = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
document.getElementById("importImage")?.addEventListener('click', () => {
|
|
129
|
+
console.log('Import image clicked');
|
|
130
|
+
isImageToolActive = true;
|
|
131
|
+
console.log('isImageToolActive set to:', isImageToolActive);
|
|
132
|
+
|
|
133
|
+
// Create a file input element
|
|
134
|
+
const fileInput = document.createElement('input');
|
|
135
|
+
fileInput.type = 'file';
|
|
136
|
+
fileInput.accept = 'image/*'; // Accept all image types
|
|
137
|
+
fileInput.style.display = 'none'; // Hide the input element
|
|
138
|
+
|
|
139
|
+
// Add the input to the document temporarily
|
|
140
|
+
document.body.appendChild(fileInput);
|
|
141
|
+
|
|
142
|
+
let fileSelected = false;
|
|
143
|
+
|
|
144
|
+
// Handle file selection
|
|
145
|
+
fileInput.addEventListener('change', (event) => {
|
|
146
|
+
const file = event.target.files[0];
|
|
147
|
+
if (file) {
|
|
148
|
+
fileSelected = true;
|
|
149
|
+
handleImageUpload(file);
|
|
150
|
+
}
|
|
151
|
+
document.body.removeChild(fileInput);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Detect cancel
|
|
155
|
+
const onFocus = () => {
|
|
156
|
+
window.removeEventListener('focus', onFocus);
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
if (!fileSelected) {
|
|
159
|
+
isImageToolActive = false;
|
|
160
|
+
if (window.__sketchEngine?.setActiveTool) {
|
|
161
|
+
window.__sketchEngine.setActiveTool('select');
|
|
162
|
+
}
|
|
163
|
+
if (fileInput.parentNode) {
|
|
164
|
+
document.body.removeChild(fileInput);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, 300);
|
|
168
|
+
};
|
|
169
|
+
window.addEventListener('focus', onFocus);
|
|
170
|
+
|
|
171
|
+
// Trigger the file picker
|
|
172
|
+
fileInput.click();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
const loadHardcodedImage = (imagePath) => {
|
|
177
|
+
console.log('Loading hardcoded image:', imagePath);
|
|
178
|
+
const img = new Image();
|
|
179
|
+
img.onload = () => {
|
|
180
|
+
console.log('Image loaded successfully');
|
|
181
|
+
const canvas = document.createElement('canvas');
|
|
182
|
+
const ctx = canvas.getContext('2d');
|
|
183
|
+
canvas.width = img.width;
|
|
184
|
+
canvas.height = img.height;
|
|
185
|
+
ctx.drawImage(img, 0, 0);
|
|
186
|
+
imageToPlace = canvas.toDataURL();
|
|
187
|
+
isDraggingImage = true;
|
|
188
|
+
console.log('Image ready to place, isDraggingImage:', isDraggingImage);
|
|
189
|
+
};
|
|
190
|
+
img.onerror = (error) => {
|
|
191
|
+
console.error('Failed to load the hardcoded image:', error);
|
|
192
|
+
console.error('Image path:', imagePath);
|
|
193
|
+
};
|
|
194
|
+
img.src = imagePath;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleImageUpload = (file) => {
|
|
198
|
+
if (!file || !isImageToolActive) return;
|
|
199
|
+
|
|
200
|
+
// Validate file type
|
|
201
|
+
if (!file.type.startsWith('image/')) {
|
|
202
|
+
console.error('Selected file is not an image');
|
|
203
|
+
alert('Please select a valid image file.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Per-room 5MB total image limit
|
|
208
|
+
if (window.__roomImageBytesUsed + file.size > ROOM_IMAGE_LIMIT_BYTES) {
|
|
209
|
+
const usedMB = (window.__roomImageBytesUsed / (1024 * 1024)).toFixed(2);
|
|
210
|
+
const fileMB = (file.size / (1024 * 1024)).toFixed(2);
|
|
211
|
+
alert(`Room image limit reached (5 MB). Used: ${usedMB} MB, this file: ${fileMB} MB. Delete some images to free space.`);
|
|
212
|
+
isImageToolActive = false;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const maxSize = 5 * 1024 * 1024; // 5MB per file (matches room limit)
|
|
217
|
+
if (file.size > maxSize) {
|
|
218
|
+
console.error('File size too large');
|
|
219
|
+
alert('Image file is too large. Please select an image smaller than 5 MB.');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log('Processing image file:', file.name, 'Size:', file.size, 'Type:', file.type);
|
|
224
|
+
|
|
225
|
+
const reader = new FileReader();
|
|
226
|
+
|
|
227
|
+
// Store file size for room limit tracking
|
|
228
|
+
window.__pendingImageFileSize = file.size;
|
|
229
|
+
|
|
230
|
+
reader.onload = (e) => {
|
|
231
|
+
imageToPlace = e.target.result;
|
|
232
|
+
isDraggingImage = true;
|
|
233
|
+
console.log('Image loaded and ready to place');
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
reader.onerror = (error) => {
|
|
237
|
+
console.error('Error reading file:', error);
|
|
238
|
+
alert('Error reading the image file. Please try again.');
|
|
239
|
+
isImageToolActive = false;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
reader.readAsDataURL(file);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Add coordinate conversion function like in other tools
|
|
246
|
+
function getSVGCoordsFromMouse(e) {
|
|
247
|
+
const viewBox = svg.viewBox.baseVal;
|
|
248
|
+
const rect = svg.getBoundingClientRect();
|
|
249
|
+
const mouseX = e.clientX - rect.left;
|
|
250
|
+
const mouseY = e.clientY - rect.top;
|
|
251
|
+
const svgX = viewBox.x + (mouseX / rect.width) * viewBox.width;
|
|
252
|
+
const svgY = viewBox.y + (mouseY / rect.height) * viewBox.height;
|
|
253
|
+
return { x: svgX, y: svgY };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Event handler for mousemove on the SVG
|
|
257
|
+
const handleMouseMoveImage = (e) => {
|
|
258
|
+
if (!isDraggingImage || !imageToPlace || !isImageToolActive) return; // Also check isImageToolActive
|
|
259
|
+
|
|
260
|
+
// Get mouse coordinates relative to the SVG element
|
|
261
|
+
const { x, y } = getSVGCoordsFromMouse(e);
|
|
262
|
+
imageX = x;
|
|
263
|
+
imageY = y;
|
|
264
|
+
|
|
265
|
+
drawMiniatureImage();
|
|
266
|
+
|
|
267
|
+
// Check for frame containment while placing image (but don't apply clipping yet)
|
|
268
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
269
|
+
shapes.forEach(frame => {
|
|
270
|
+
if (frame.shapeName === 'frame') {
|
|
271
|
+
// Create temporary image bounds for frame checking
|
|
272
|
+
const tempImageBounds = {
|
|
273
|
+
x: imageX - 50, // Half of miniature width
|
|
274
|
+
y: imageY - 50, // Approximate half height
|
|
275
|
+
width: 100,
|
|
276
|
+
height: 100
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (frame.isShapeInFrame(tempImageBounds)) {
|
|
280
|
+
frame.highlightFrame();
|
|
281
|
+
hoveredFrameImage = frame;
|
|
282
|
+
} else if (hoveredFrameImage === frame) {
|
|
283
|
+
frame.removeHighlight();
|
|
284
|
+
hoveredFrameImage = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const drawMiniatureImage = () => {
|
|
292
|
+
if (!isDraggingImage || !imageToPlace || !isImageToolActive) return; // Also check isImageToolActive
|
|
293
|
+
|
|
294
|
+
const miniatureWidth = 100; // Fixed width for miniature (adjust as needed)
|
|
295
|
+
getImageAspectRatio(imageToPlace)
|
|
296
|
+
.then(aspectRatio => {
|
|
297
|
+
const miniatureHeight = miniatureWidth * aspectRatio; // Maintain aspect ratio
|
|
298
|
+
|
|
299
|
+
// Remove the previous miniature image, if it exists
|
|
300
|
+
if (currentImageElement) {
|
|
301
|
+
svg.removeChild(currentImageElement);
|
|
302
|
+
currentImageElement = null; // Important: clear the reference
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Create an SVG image element for the miniature
|
|
306
|
+
currentImageElement = document.createElementNS("http://www.w3.org/2000/svg", "image");
|
|
307
|
+
currentImageElement.setAttribute("href", imageToPlace);
|
|
308
|
+
currentImageElement.setAttribute("x", imageX - miniatureWidth / 2);
|
|
309
|
+
currentImageElement.setAttribute("y", imageY - miniatureHeight / 2);
|
|
310
|
+
currentImageElement.setAttribute("width", miniatureWidth);
|
|
311
|
+
currentImageElement.setAttribute("height", miniatureHeight);
|
|
312
|
+
currentImageElement.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
313
|
+
|
|
314
|
+
svg.appendChild(currentImageElement);
|
|
315
|
+
})
|
|
316
|
+
.catch(error => {
|
|
317
|
+
console.error("Error getting aspect ratio:", error);
|
|
318
|
+
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
function getImageAspectRatio(dataUrl) {
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const img = new Image();
|
|
325
|
+
img.onload = () => {
|
|
326
|
+
resolve(img.height / img.width);
|
|
327
|
+
};
|
|
328
|
+
img.onerror = () => {
|
|
329
|
+
reject(new Error('Failed to load image for aspect ratio calculation.'));
|
|
330
|
+
};
|
|
331
|
+
img.src = dataUrl;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Update the handleMouseDownImage function to create proper group structure
|
|
336
|
+
const handleMouseDownImage = async (e) => {
|
|
337
|
+
if (!isDraggingImage || !imageToPlace || !isImageToolActive) {
|
|
338
|
+
// Handle image selection if selection tool is active
|
|
339
|
+
if (isSelectionToolActive) {
|
|
340
|
+
const clickedImage = e.target.closest('image');
|
|
341
|
+
if (clickedImage) {
|
|
342
|
+
selectImage({ target: clickedImage, stopPropagation: () => e.stopPropagation() });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
//Get aspect ratio before we clear the temporary image data.
|
|
351
|
+
let aspectRatio = await getImageAspectRatio(imageToPlace);
|
|
352
|
+
|
|
353
|
+
// Remove the miniature
|
|
354
|
+
if (currentImageElement) {
|
|
355
|
+
svg.removeChild(currentImageElement);
|
|
356
|
+
currentImageElement = null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Calculate actual dimensions of the placed image
|
|
360
|
+
const placedImageWidth = 200; // Adjust as needed
|
|
361
|
+
const placedImageHeight = placedImageWidth * aspectRatio;
|
|
362
|
+
|
|
363
|
+
// Get SVG coordinates
|
|
364
|
+
const { x: placedX, y: placedY } = getSVGCoordsFromMouse(e);
|
|
365
|
+
|
|
366
|
+
// Create a new SVG image element for the final image
|
|
367
|
+
const finalImage = document.createElementNS("http://www.w3.org/2000/svg", "image");
|
|
368
|
+
finalImage.setAttribute("href", imageToPlace);
|
|
369
|
+
finalImage.setAttribute("x", placedX - placedImageWidth / 2);
|
|
370
|
+
finalImage.setAttribute("y", placedY - placedImageHeight / 2);
|
|
371
|
+
finalImage.setAttribute("width", placedImageWidth);
|
|
372
|
+
finalImage.setAttribute("height", placedImageHeight);
|
|
373
|
+
finalImage.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
374
|
+
|
|
375
|
+
// Store data for undo/redo
|
|
376
|
+
finalImage.setAttribute('data-href', imageToPlace);
|
|
377
|
+
finalImage.setAttribute('data-x', placedX - placedImageWidth / 2);
|
|
378
|
+
finalImage.setAttribute('data-y', placedY - placedImageHeight / 2);
|
|
379
|
+
finalImage.setAttribute('data-width', placedImageWidth);
|
|
380
|
+
finalImage.setAttribute('data-height', placedImageHeight);
|
|
381
|
+
|
|
382
|
+
// Add arrow attachment support data attributes
|
|
383
|
+
finalImage.setAttribute('type', 'image');
|
|
384
|
+
finalImage.setAttribute('data-shape-x', placedX - placedImageWidth / 2);
|
|
385
|
+
finalImage.setAttribute('data-shape-y', placedY - placedImageHeight / 2);
|
|
386
|
+
finalImage.setAttribute('data-shape-width', placedImageWidth);
|
|
387
|
+
finalImage.setAttribute('data-shape-height', placedImageHeight);
|
|
388
|
+
finalImage.setAttribute('data-shape-rotation', 0);
|
|
389
|
+
finalImage.shapeID = `image-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
|
|
390
|
+
|
|
391
|
+
// Don't add to SVG directly - let ImageShape wrapper handle it
|
|
392
|
+
// svg.appendChild(finalImage);
|
|
393
|
+
|
|
394
|
+
// Create ImageShape wrapper for frame functionality
|
|
395
|
+
const imageShape = wrapImageElement(finalImage);
|
|
396
|
+
|
|
397
|
+
// Add to shapes array for arrow attachment and frame functionality
|
|
398
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
399
|
+
shapes.push(imageShape);
|
|
400
|
+
console.log('Image added to shapes array for arrow attachment and frame functionality');
|
|
401
|
+
} else {
|
|
402
|
+
console.warn('shapes array not found - arrows and frames may not work with images');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check for frame containment and track attachment
|
|
406
|
+
const finalFrame = hoveredFrameImage;
|
|
407
|
+
if (finalFrame) {
|
|
408
|
+
finalFrame.addShapeToFrame(imageShape);
|
|
409
|
+
// Track the attachment for undo
|
|
410
|
+
pushFrameAttachmentAction(finalFrame, imageShape, 'attach', null);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Add to undo stack for image creation
|
|
414
|
+
pushCreateAction({
|
|
415
|
+
type: 'image',
|
|
416
|
+
element: imageShape,
|
|
417
|
+
remove: () => {
|
|
418
|
+
if (imageShape.group && imageShape.group.parentNode) {
|
|
419
|
+
imageShape.group.parentNode.removeChild(imageShape.group);
|
|
420
|
+
}
|
|
421
|
+
// Remove from shapes array
|
|
422
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
423
|
+
const idx = shapes.indexOf(imageShape);
|
|
424
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
425
|
+
}
|
|
426
|
+
// Clean up arrow attachments when image is removed
|
|
427
|
+
if (typeof cleanupAttachments === 'function') {
|
|
428
|
+
cleanupAttachments(finalImage);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
restore: () => {
|
|
432
|
+
svg.appendChild(imageShape.group);
|
|
433
|
+
// Add back to shapes array
|
|
434
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
435
|
+
if (shapes.indexOf(imageShape) === -1) {
|
|
436
|
+
shapes.push(imageShape);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Track image size for room limit
|
|
443
|
+
const placedFileSize = window.__pendingImageFileSize || 0;
|
|
444
|
+
finalImage.__fileSize = placedFileSize;
|
|
445
|
+
window.__roomImageBytesUsed = (window.__roomImageBytesUsed || 0) + placedFileSize;
|
|
446
|
+
window.__pendingImageFileSize = 0;
|
|
447
|
+
|
|
448
|
+
// Add click event to the newly added image
|
|
449
|
+
finalImage.addEventListener('click', selectImage);
|
|
450
|
+
|
|
451
|
+
// Clear frame highlighting
|
|
452
|
+
if (hoveredFrameImage) {
|
|
453
|
+
hoveredFrameImage.removeHighlight();
|
|
454
|
+
hoveredFrameImage = null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Auto-select the placed image and switch to selection tool
|
|
458
|
+
const placedShape = imageShape;
|
|
459
|
+
if (window.__sketchStoreApi) window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
|
|
460
|
+
currentShape = placedShape;
|
|
461
|
+
currentShape.isSelected = true;
|
|
462
|
+
placedShape.selectShape();
|
|
463
|
+
|
|
464
|
+
// Fire async upload pipeline (compress + upload to Cloudinary)
|
|
465
|
+
uploadImageToCloudinary(imageShape).catch(err => {
|
|
466
|
+
console.warn('[ImageTool] Upload pipeline error:', err);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error("Error placing image:", error);
|
|
471
|
+
isDraggingImage = false;
|
|
472
|
+
imageToPlace = null;
|
|
473
|
+
isImageToolActive = false; // Important: Reset the tool state.
|
|
474
|
+
} finally {
|
|
475
|
+
isDraggingImage = false;
|
|
476
|
+
imageToPlace = null;
|
|
477
|
+
isImageToolActive = false; // Important: Reset the tool state.
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const handleMouseUpImage = (e) => {
|
|
482
|
+
// Only deselect if the user actually clicked (mousedown+mouseup) on a non-image area
|
|
483
|
+
// of the SVG canvas — not when the mouse just leaves to the UI
|
|
484
|
+
if (isSelectionToolActive && selectedImage) {
|
|
485
|
+
const clickedElement = e.target;
|
|
486
|
+
const isOnSVG = clickedElement === svg || clickedElement.ownerSVGElement === svg;
|
|
487
|
+
const isImageElement = clickedElement.tagName === 'image';
|
|
488
|
+
const isAnchorElement = clickedElement.classList.contains('resize-anchor') ||
|
|
489
|
+
clickedElement.classList.contains('rotation-anchor') ||
|
|
490
|
+
clickedElement.classList.contains('selection-outline');
|
|
491
|
+
|
|
492
|
+
// Only deselect if mouseup is directly on the SVG background (not on any shape/anchor)
|
|
493
|
+
if (isOnSVG && !isImageElement && !isAnchorElement && clickedElement === svg && !isDragging && !isRotatingImage && !currentAnchor) {
|
|
494
|
+
removeSelectionOutline();
|
|
495
|
+
selectedImage = null;
|
|
496
|
+
if (window.__sketchStoreApi) {
|
|
497
|
+
window.__sketchStoreApi.clearSelectedShapeSidebar();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Clear frame highlighting if placing image
|
|
503
|
+
if (hoveredFrameImage) {
|
|
504
|
+
hoveredFrameImage.removeHighlight();
|
|
505
|
+
hoveredFrameImage = null;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
function selectImage(event) {
|
|
510
|
+
if (!isSelectionToolActive) return;
|
|
511
|
+
|
|
512
|
+
event.stopPropagation(); // Prevent click from propagating to the SVG
|
|
513
|
+
|
|
514
|
+
if (selectedImage) {
|
|
515
|
+
removeSelectionOutline();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
selectedImage = event.target;
|
|
519
|
+
|
|
520
|
+
// Get the current rotation from the image transform attribute
|
|
521
|
+
const transform = selectedImage.getAttribute('transform');
|
|
522
|
+
if (transform) {
|
|
523
|
+
const rotateMatch = transform.match(/rotate\(([^,]+)/);
|
|
524
|
+
if (rotateMatch) {
|
|
525
|
+
imageRotation = parseFloat(rotateMatch[1]);
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
imageRotation = 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
addSelectionOutline();
|
|
532
|
+
|
|
533
|
+
// Store original dimensions for resizing
|
|
534
|
+
originalX = parseFloat(selectedImage.getAttribute('x'));
|
|
535
|
+
originalY = parseFloat(selectedImage.getAttribute('y'));
|
|
536
|
+
originalWidth = parseFloat(selectedImage.getAttribute('width'));
|
|
537
|
+
originalHeight = parseFloat(selectedImage.getAttribute('height'));
|
|
538
|
+
|
|
539
|
+
// Add drag event listeners to the selected image
|
|
540
|
+
selectedImage.addEventListener('mousedown', startDrag);
|
|
541
|
+
selectedImage.addEventListener('mouseup', stopDrag);
|
|
542
|
+
selectedImage.addEventListener('mouseleave', stopDrag);
|
|
543
|
+
|
|
544
|
+
// Set currentShape for sidebar + layer controls
|
|
545
|
+
const imageShape = (typeof shapes !== 'undefined' && Array.isArray(shapes))
|
|
546
|
+
? shapes.find(s => s.shapeName === 'image' && s.element === selectedImage)
|
|
547
|
+
: null;
|
|
548
|
+
if (imageShape) {
|
|
549
|
+
window.currentShape = imageShape;
|
|
550
|
+
}
|
|
551
|
+
if (typeof window.__showSidebarForShape === 'function') {
|
|
552
|
+
window.__showSidebarForShape('image');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function addSelectionOutline() {
|
|
557
|
+
if (!selectedImage) return;
|
|
558
|
+
|
|
559
|
+
const x = parseFloat(selectedImage.getAttribute('x'));
|
|
560
|
+
const y = parseFloat(selectedImage.getAttribute('y'));
|
|
561
|
+
const width = parseFloat(selectedImage.getAttribute('width'));
|
|
562
|
+
const height = parseFloat(selectedImage.getAttribute('height'));
|
|
563
|
+
|
|
564
|
+
const selectionPadding = 8; // Padding around the selection
|
|
565
|
+
const expandedX = x - selectionPadding;
|
|
566
|
+
const expandedY = y - selectionPadding;
|
|
567
|
+
const expandedWidth = width + 2 * selectionPadding;
|
|
568
|
+
const expandedHeight = height + 2 * selectionPadding;
|
|
569
|
+
|
|
570
|
+
// Create a dashed outline
|
|
571
|
+
const outlinePoints = [
|
|
572
|
+
[expandedX, expandedY],
|
|
573
|
+
[expandedX + expandedWidth, expandedY],
|
|
574
|
+
[expandedX + expandedWidth, expandedY + expandedHeight],
|
|
575
|
+
[expandedX, expandedY + expandedHeight],
|
|
576
|
+
[expandedX, expandedY]
|
|
577
|
+
];
|
|
578
|
+
const pointsAttr = outlinePoints.map(p => p.join(',')).join(' ');
|
|
579
|
+
const outline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
580
|
+
outline.setAttribute("points", pointsAttr);
|
|
581
|
+
outline.setAttribute("fill", "none");
|
|
582
|
+
outline.setAttribute("stroke", "#5B57D1");
|
|
583
|
+
outline.setAttribute("stroke-width", 1.5);
|
|
584
|
+
outline.setAttribute("stroke-dasharray", "4 2");
|
|
585
|
+
outline.setAttribute("style", "pointer-events: none;");
|
|
586
|
+
outline.setAttribute("class", "selection-outline");
|
|
587
|
+
|
|
588
|
+
// Apply the same rotation as the image
|
|
589
|
+
const centerX = x + width / 2;
|
|
590
|
+
const centerY = y + height / 2;
|
|
591
|
+
outline.setAttribute('transform', `rotate(${imageRotation}, ${centerX}, ${centerY})`);
|
|
592
|
+
|
|
593
|
+
svg.appendChild(outline);
|
|
594
|
+
|
|
595
|
+
// Add resize anchors
|
|
596
|
+
addResizeAnchors(expandedX, expandedY, expandedWidth, expandedHeight, centerX, centerY);
|
|
597
|
+
|
|
598
|
+
// Add rotation anchor
|
|
599
|
+
addRotationAnchor(expandedX, expandedY, expandedWidth, expandedHeight, centerX, centerY);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function removeSelectionOutline() {
|
|
603
|
+
// Remove ALL selection outlines (querySelectorAll to prevent stacking)
|
|
604
|
+
svg.querySelectorAll(".selection-outline").forEach(el => el.remove());
|
|
605
|
+
|
|
606
|
+
// Remove resize anchors
|
|
607
|
+
removeResizeAnchors();
|
|
608
|
+
|
|
609
|
+
// Remove rotation anchor
|
|
610
|
+
removeRotationAnchor();
|
|
611
|
+
|
|
612
|
+
// Remove drag event listeners
|
|
613
|
+
if (selectedImage) {
|
|
614
|
+
selectedImage.removeEventListener('mousedown', startDrag);
|
|
615
|
+
selectedImage.removeEventListener('mouseup', stopDrag);
|
|
616
|
+
selectedImage.removeEventListener('mouseleave', stopDrag);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function addResizeAnchors(x, y, width, height, centerX, centerY) {
|
|
621
|
+
const zoom = window.currentZoom || 1;
|
|
622
|
+
const anchorSize = 10 / zoom;
|
|
623
|
+
const anchorStrokeWidth = 2;
|
|
624
|
+
|
|
625
|
+
const positions = [
|
|
626
|
+
{ x: x, y: y }, // Top-left
|
|
627
|
+
{ x: x + width, y: y }, // Top-right
|
|
628
|
+
{ x: x, y: y + height }, // Bottom-left
|
|
629
|
+
{ x: x + width, y: y + height } // Bottom-right
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
positions.forEach((pos, i) => {
|
|
633
|
+
const anchor = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
634
|
+
anchor.setAttribute("x", pos.x - anchorSize / 2);
|
|
635
|
+
anchor.setAttribute("y", pos.y - anchorSize / 2);
|
|
636
|
+
anchor.setAttribute("width", anchorSize);
|
|
637
|
+
anchor.setAttribute("height", anchorSize);
|
|
638
|
+
anchor.setAttribute("fill", "#121212");
|
|
639
|
+
anchor.setAttribute("stroke", "#5B57D1");
|
|
640
|
+
anchor.setAttribute("stroke-width", anchorStrokeWidth);
|
|
641
|
+
anchor.setAttribute("class", "resize-anchor");
|
|
642
|
+
anchor.style.cursor = ["nw-resize", "ne-resize", "sw-resize", "se-resize"][i];
|
|
643
|
+
|
|
644
|
+
// Apply the same rotation as the image
|
|
645
|
+
anchor.setAttribute('transform', `rotate(${imageRotation}, ${centerX}, ${centerY})`);
|
|
646
|
+
|
|
647
|
+
svg.appendChild(anchor);
|
|
648
|
+
|
|
649
|
+
// Add event listeners for resizing
|
|
650
|
+
anchor.addEventListener('mousedown', startResize);
|
|
651
|
+
anchor.addEventListener('mouseup', stopResize);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function addRotationAnchor(x, y, width, height, centerX, centerY) {
|
|
656
|
+
const anchorStrokeWidth = 2;
|
|
657
|
+
const rotationAnchorPos = { x: x + width / 2, y: y - 30 };
|
|
658
|
+
|
|
659
|
+
const rotationAnchor = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
660
|
+
rotationAnchor.setAttribute('cx', rotationAnchorPos.x);
|
|
661
|
+
rotationAnchor.setAttribute('cy', rotationAnchorPos.y);
|
|
662
|
+
rotationAnchor.setAttribute('r', 8);
|
|
663
|
+
rotationAnchor.setAttribute('class', 'rotation-anchor');
|
|
664
|
+
rotationAnchor.setAttribute('fill', '#121212');
|
|
665
|
+
rotationAnchor.setAttribute('stroke', '#5B57D1');
|
|
666
|
+
rotationAnchor.setAttribute('stroke-width', anchorStrokeWidth);
|
|
667
|
+
rotationAnchor.setAttribute('style', 'pointer-events: all;');
|
|
668
|
+
|
|
669
|
+
// Apply the same rotation as the image
|
|
670
|
+
rotationAnchor.setAttribute('transform', `rotate(${imageRotation}, ${centerX}, ${centerY})`);
|
|
671
|
+
|
|
672
|
+
svg.appendChild(rotationAnchor);
|
|
673
|
+
|
|
674
|
+
// Add event listeners for rotation
|
|
675
|
+
rotationAnchor.addEventListener('mousedown', startRotation);
|
|
676
|
+
rotationAnchor.addEventListener('mouseup', stopRotation);
|
|
677
|
+
|
|
678
|
+
rotationAnchor.addEventListener('mouseover', function () {
|
|
679
|
+
if (!isRotatingImage && !isDragging) {
|
|
680
|
+
rotationAnchor.style.cursor = 'grab';
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
rotationAnchor.addEventListener('mouseout', function () {
|
|
685
|
+
if (!isRotatingImage && !isDragging) {
|
|
686
|
+
rotationAnchor.style.cursor = 'default';
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function removeRotationAnchor() {
|
|
692
|
+
svg.querySelectorAll(".rotation-anchor").forEach(el => el.remove());
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function addAnchor(x, y, cursor) {
|
|
696
|
+
const anchorSize = 8 / (window.currentZoom || 1);
|
|
697
|
+
const anchor = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
698
|
+
anchor.setAttribute("x", x);
|
|
699
|
+
anchor.setAttribute("y", y);
|
|
700
|
+
anchor.setAttribute("width", anchorSize);
|
|
701
|
+
anchor.setAttribute("height", anchorSize);
|
|
702
|
+
anchor.setAttribute("fill", "white");
|
|
703
|
+
anchor.setAttribute("stroke", "black");
|
|
704
|
+
anchor.setAttribute("stroke-width", "1");
|
|
705
|
+
anchor.setAttribute("class", "resize-anchor"); // For easy removal
|
|
706
|
+
anchor.style.cursor = cursor;
|
|
707
|
+
|
|
708
|
+
svg.appendChild(anchor);
|
|
709
|
+
|
|
710
|
+
// Add event listeners for dragging
|
|
711
|
+
anchor.addEventListener('mousedown', startResize);
|
|
712
|
+
anchor.addEventListener('mouseup', stopResize);
|
|
713
|
+
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function removeResizeAnchors() {
|
|
717
|
+
const anchors = svg.querySelectorAll(".resize-anchor");
|
|
718
|
+
anchors.forEach(anchor => svg.removeChild(anchor));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function startResize(event) {
|
|
722
|
+
event.preventDefault();
|
|
723
|
+
event.stopPropagation();
|
|
724
|
+
|
|
725
|
+
currentAnchor = event.target;
|
|
726
|
+
|
|
727
|
+
// Store original values at the start of resize
|
|
728
|
+
originalX = parseFloat(selectedImage.getAttribute('x'));
|
|
729
|
+
originalY = parseFloat(selectedImage.getAttribute('y'));
|
|
730
|
+
originalWidth = parseFloat(selectedImage.getAttribute('width'));
|
|
731
|
+
originalHeight = parseFloat(selectedImage.getAttribute('height'));
|
|
732
|
+
|
|
733
|
+
// Store original rotation
|
|
734
|
+
const transform = selectedImage.getAttribute('transform');
|
|
735
|
+
if (transform) {
|
|
736
|
+
const rotateMatch = transform.match(/rotate\(([^,]+)/);
|
|
737
|
+
if (rotateMatch) {
|
|
738
|
+
imageRotation = parseFloat(rotateMatch[1]);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
svg.addEventListener('mousemove', resizeImage);
|
|
743
|
+
document.addEventListener('mouseup', stopResize);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function stopResize(event) {
|
|
747
|
+
stopInteracting(); // Call the combined stop function
|
|
748
|
+
document.removeEventListener('mouseup', stopResize); // Remove the global mouseup listener
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function resizeImage(event) {
|
|
752
|
+
if (!selectedImage || !currentAnchor) return;
|
|
753
|
+
|
|
754
|
+
const { x: globalX, y: globalY } = getSVGCoordsFromMouse(event);
|
|
755
|
+
|
|
756
|
+
// Use ORIGINAL center for consistent inverse rotation (avoids drift)
|
|
757
|
+
const centerX = originalX + originalWidth / 2;
|
|
758
|
+
const centerY = originalY + originalHeight / 2;
|
|
759
|
+
|
|
760
|
+
// Convert mouse position to local coordinates accounting for rotation
|
|
761
|
+
let localX = globalX;
|
|
762
|
+
let localY = globalY;
|
|
763
|
+
|
|
764
|
+
if (imageRotation !== 0) {
|
|
765
|
+
const rotationRad = (imageRotation * Math.PI) / 180;
|
|
766
|
+
const translatedX = globalX - centerX;
|
|
767
|
+
const translatedY = globalY - centerY;
|
|
768
|
+
localX = translatedX * Math.cos(-rotationRad) - translatedY * Math.sin(-rotationRad) + centerX;
|
|
769
|
+
localY = translatedX * Math.sin(-rotationRad) + translatedY * Math.cos(-rotationRad) + centerY;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Calculate resize deltas in local coordinate space
|
|
773
|
+
let dx = localX - originalX;
|
|
774
|
+
let dy = localY - originalY;
|
|
775
|
+
|
|
776
|
+
let newWidth = originalWidth;
|
|
777
|
+
let newHeight = originalHeight;
|
|
778
|
+
let newX = originalX;
|
|
779
|
+
let newY = originalY;
|
|
780
|
+
const aspectRatio = originalHeight / originalWidth;
|
|
781
|
+
|
|
782
|
+
// Track which corner is fixed (relative to original rect)
|
|
783
|
+
let fixedRelX, fixedRelY;
|
|
784
|
+
|
|
785
|
+
switch (currentAnchor.style.cursor) {
|
|
786
|
+
case "nw-resize":
|
|
787
|
+
newWidth = originalWidth - dx;
|
|
788
|
+
newHeight = originalHeight - dy;
|
|
789
|
+
if (aspect_ratio_lock) {
|
|
790
|
+
newHeight = newWidth * aspectRatio;
|
|
791
|
+
dy = originalHeight - newHeight;
|
|
792
|
+
}
|
|
793
|
+
newX = originalX + (originalWidth - newWidth);
|
|
794
|
+
newY = originalY + (originalHeight - newHeight);
|
|
795
|
+
fixedRelX = originalWidth; fixedRelY = originalHeight;
|
|
796
|
+
break;
|
|
797
|
+
case "ne-resize":
|
|
798
|
+
newWidth = dx;
|
|
799
|
+
newHeight = originalHeight - dy;
|
|
800
|
+
if (aspect_ratio_lock) {
|
|
801
|
+
newHeight = newWidth * aspectRatio;
|
|
802
|
+
dy = originalHeight - newHeight;
|
|
803
|
+
}
|
|
804
|
+
newX = originalX;
|
|
805
|
+
newY = originalY + (originalHeight - newHeight);
|
|
806
|
+
fixedRelX = 0; fixedRelY = originalHeight;
|
|
807
|
+
break;
|
|
808
|
+
case "sw-resize":
|
|
809
|
+
newWidth = originalWidth - dx;
|
|
810
|
+
newHeight = dy;
|
|
811
|
+
if (aspect_ratio_lock) {
|
|
812
|
+
newHeight = newWidth * aspectRatio;
|
|
813
|
+
dy = newHeight;
|
|
814
|
+
}
|
|
815
|
+
newX = originalX + (originalWidth - newWidth);
|
|
816
|
+
newY = originalY;
|
|
817
|
+
fixedRelX = originalWidth; fixedRelY = 0;
|
|
818
|
+
break;
|
|
819
|
+
case "se-resize":
|
|
820
|
+
newWidth = dx;
|
|
821
|
+
newHeight = dy;
|
|
822
|
+
if (aspect_ratio_lock) {
|
|
823
|
+
newHeight = newWidth * aspectRatio;
|
|
824
|
+
}
|
|
825
|
+
newX = originalX;
|
|
826
|
+
newY = originalY;
|
|
827
|
+
fixedRelX = 0; fixedRelY = 0;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Ensure minimum size
|
|
832
|
+
newWidth = Math.max(minImageSize, newWidth);
|
|
833
|
+
newHeight = Math.max(minImageSize, newHeight);
|
|
834
|
+
|
|
835
|
+
// Compensate for rotation center shift when rotated
|
|
836
|
+
if (imageRotation !== 0) {
|
|
837
|
+
const rad = (imageRotation * Math.PI) / 180;
|
|
838
|
+
const cosR = Math.cos(rad);
|
|
839
|
+
const sinR = Math.sin(rad);
|
|
840
|
+
|
|
841
|
+
// Compute rotated world position of the fixed corner in the original rect
|
|
842
|
+
const oldCX = originalX + originalWidth / 2;
|
|
843
|
+
const oldCY = originalY + originalHeight / 2;
|
|
844
|
+
const odx = (originalX + fixedRelX) - oldCX;
|
|
845
|
+
const ody = (originalY + fixedRelY) - oldCY;
|
|
846
|
+
const fixedWorldX = oldCX + odx * cosR - ody * sinR;
|
|
847
|
+
const fixedWorldY = oldCY + odx * sinR + ody * cosR;
|
|
848
|
+
|
|
849
|
+
// Determine where the fixed corner sits in the new rect (relative to new origin)
|
|
850
|
+
const fixedNewRelX = fixedRelX === 0 ? 0 : newWidth;
|
|
851
|
+
const fixedNewRelY = fixedRelY === 0 ? 0 : newHeight;
|
|
852
|
+
|
|
853
|
+
// Solve for new origin so the fixed corner stays in place
|
|
854
|
+
const ncx = newWidth / 2;
|
|
855
|
+
const ncy = newHeight / 2;
|
|
856
|
+
const ndx = fixedNewRelX - ncx;
|
|
857
|
+
const ndy = fixedNewRelY - ncy;
|
|
858
|
+
const rotX = ncx + ndx * cosR - ndy * sinR;
|
|
859
|
+
const rotY = ncy + ndx * sinR + ndy * cosR;
|
|
860
|
+
|
|
861
|
+
newX = fixedWorldX - rotX;
|
|
862
|
+
newY = fixedWorldY - rotY;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Apply the new dimensions and position
|
|
866
|
+
selectedImage.setAttribute('width', newWidth);
|
|
867
|
+
selectedImage.setAttribute('height', newHeight);
|
|
868
|
+
selectedImage.setAttribute('x', newX);
|
|
869
|
+
selectedImage.setAttribute('y', newY);
|
|
870
|
+
|
|
871
|
+
// Update data attributes for arrow attachment
|
|
872
|
+
selectedImage.setAttribute('data-shape-x', newX);
|
|
873
|
+
selectedImage.setAttribute('data-shape-y', newY);
|
|
874
|
+
selectedImage.setAttribute('data-shape-width', newWidth);
|
|
875
|
+
selectedImage.setAttribute('data-shape-height', newHeight);
|
|
876
|
+
|
|
877
|
+
// Reapply the rotation transform with the new center
|
|
878
|
+
const newCenterX = newX + newWidth / 2;
|
|
879
|
+
const newCenterY = newY + newHeight / 2;
|
|
880
|
+
selectedImage.setAttribute('transform', `rotate(${imageRotation}, ${newCenterX}, ${newCenterY})`);
|
|
881
|
+
|
|
882
|
+
// Update attached arrows during resize
|
|
883
|
+
updateArrowsForShape(selectedImage);
|
|
884
|
+
|
|
885
|
+
// Update the selection outline and anchors
|
|
886
|
+
removeSelectionOutline();
|
|
887
|
+
addSelectionOutline();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function stopRotation(event) {
|
|
891
|
+
if (!isRotatingImage) return;
|
|
892
|
+
stopInteracting();
|
|
893
|
+
isRotatingImage = false;
|
|
894
|
+
startRotationMouseAngle = null;
|
|
895
|
+
startImageRotation = null;
|
|
896
|
+
svg.removeEventListener('mousemove', rotateImage);
|
|
897
|
+
document.removeEventListener('mouseup', stopRotation);
|
|
898
|
+
svg.style.cursor = 'default';
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function startDrag(event) {
|
|
902
|
+
if (!isSelectionToolActive || !selectedImage) return;
|
|
903
|
+
|
|
904
|
+
event.preventDefault();
|
|
905
|
+
event.stopPropagation();
|
|
906
|
+
|
|
907
|
+
isDragging = true;
|
|
908
|
+
|
|
909
|
+
// Store original values at the start of drag
|
|
910
|
+
originalX = parseFloat(selectedImage.getAttribute('x'));
|
|
911
|
+
originalY = parseFloat(selectedImage.getAttribute('y'));
|
|
912
|
+
originalWidth = parseFloat(selectedImage.getAttribute('width'));
|
|
913
|
+
originalHeight = parseFloat(selectedImage.getAttribute('height'));
|
|
914
|
+
|
|
915
|
+
// Store original rotation
|
|
916
|
+
const transform = selectedImage.getAttribute('transform');
|
|
917
|
+
if (transform) {
|
|
918
|
+
const rotateMatch = transform.match(/rotate\(([^,]+)/);
|
|
919
|
+
if (rotateMatch) {
|
|
920
|
+
imageRotation = parseFloat(rotateMatch[1]);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Find the ImageShape wrapper for frame functionality
|
|
925
|
+
let imageShape = null;
|
|
926
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
927
|
+
imageShape = shapes.find(shape => shape.shapeName === 'image' && shape.element === selectedImage);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (imageShape) {
|
|
931
|
+
// Store initial frame state
|
|
932
|
+
draggedShapeInitialFrameImage = imageShape.parentFrame || null;
|
|
933
|
+
|
|
934
|
+
// Temporarily remove from frame clipping if dragging
|
|
935
|
+
if (imageShape.parentFrame) {
|
|
936
|
+
imageShape.parentFrame.temporarilyRemoveFromFrame(imageShape);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const { x, y } = getSVGCoordsFromMouse(event);
|
|
941
|
+
dragOffsetX = x - parseFloat(selectedImage.getAttribute('x'));
|
|
942
|
+
dragOffsetY = y - parseFloat(selectedImage.getAttribute('y'));
|
|
943
|
+
|
|
944
|
+
svg.addEventListener('mousemove', dragImage);
|
|
945
|
+
document.addEventListener('mouseup', stopDrag);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function dragImage(event) {
|
|
949
|
+
if (!isDragging || !selectedImage) return;
|
|
950
|
+
|
|
951
|
+
const { x, y } = getSVGCoordsFromMouse(event);
|
|
952
|
+
let newX = x - dragOffsetX;
|
|
953
|
+
let newY = y - dragOffsetY;
|
|
954
|
+
|
|
955
|
+
selectedImage.setAttribute('x', newX);
|
|
956
|
+
selectedImage.setAttribute('y', newY);
|
|
957
|
+
|
|
958
|
+
// Update data attributes for arrow attachment
|
|
959
|
+
selectedImage.setAttribute('data-shape-x', newX);
|
|
960
|
+
selectedImage.setAttribute('data-shape-y', newY);
|
|
961
|
+
|
|
962
|
+
// Reapply the rotation transform with the new position
|
|
963
|
+
const newCenterX = newX + parseFloat(selectedImage.getAttribute('width')) / 2;
|
|
964
|
+
const newCenterY = newY + parseFloat(selectedImage.getAttribute('height')) / 2;
|
|
965
|
+
selectedImage.setAttribute('transform', `rotate(${imageRotation}, ${newCenterX}, ${newCenterY})`);
|
|
966
|
+
|
|
967
|
+
// Update frame containment for ImageShape wrapper
|
|
968
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
969
|
+
const imageShape = shapes.find(shape => shape.shapeName === 'image' && shape.element === selectedImage);
|
|
970
|
+
if (imageShape) {
|
|
971
|
+
imageShape.updateFrameContainment();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Update attached arrows during drag
|
|
976
|
+
updateArrowsForShape(selectedImage);
|
|
977
|
+
|
|
978
|
+
// Update the selection outline and anchors
|
|
979
|
+
removeSelectionOutline();
|
|
980
|
+
addSelectionOutline();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function stopDrag(event) {
|
|
984
|
+
stopInteracting(); // Call the combined stop function
|
|
985
|
+
document.removeEventListener('mouseup', stopDrag); // Remove the global mouseup listener
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function startRotation(event) {
|
|
989
|
+
event.preventDefault();
|
|
990
|
+
event.stopPropagation();
|
|
991
|
+
|
|
992
|
+
if (!selectedImage) return;
|
|
993
|
+
|
|
994
|
+
isRotatingImage = true;
|
|
995
|
+
|
|
996
|
+
// Get image center
|
|
997
|
+
const imgX = parseFloat(selectedImage.getAttribute('x'));
|
|
998
|
+
const imgY = parseFloat(selectedImage.getAttribute('y'));
|
|
999
|
+
const imgWidth = parseFloat(selectedImage.getAttribute('width'));
|
|
1000
|
+
const imgHeight = parseFloat(selectedImage.getAttribute('height'));
|
|
1001
|
+
|
|
1002
|
+
const centerX = imgX + imgWidth / 2;
|
|
1003
|
+
const centerY = imgY + imgHeight / 2;
|
|
1004
|
+
|
|
1005
|
+
// Calculate initial mouse angle relative to image center
|
|
1006
|
+
const { x: mouseX, y: mouseY } = getSVGCoordsFromMouse(event);
|
|
1007
|
+
|
|
1008
|
+
startRotationMouseAngle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI;
|
|
1009
|
+
startImageRotation = imageRotation;
|
|
1010
|
+
|
|
1011
|
+
svg.addEventListener('mousemove', rotateImage);
|
|
1012
|
+
document.addEventListener('mouseup', stopRotation);
|
|
1013
|
+
|
|
1014
|
+
svg.style.cursor = 'grabbing';
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function rotateImage(event) {
|
|
1018
|
+
if (!isRotatingImage || !selectedImage) return;
|
|
1019
|
+
|
|
1020
|
+
// Get image center
|
|
1021
|
+
const imgX = parseFloat(selectedImage.getAttribute('x'));
|
|
1022
|
+
const imgY = parseFloat(selectedImage.getAttribute('y'));
|
|
1023
|
+
const imgWidth = parseFloat(selectedImage.getAttribute('width'));
|
|
1024
|
+
const imgHeight = parseFloat(selectedImage.getAttribute('height'));
|
|
1025
|
+
|
|
1026
|
+
const centerX = imgX + imgWidth / 2;
|
|
1027
|
+
const centerY = imgY + imgHeight / 2;
|
|
1028
|
+
|
|
1029
|
+
// Calculate current mouse angle
|
|
1030
|
+
const { x: mouseX, y: mouseY } = getSVGCoordsFromMouse(event);
|
|
1031
|
+
|
|
1032
|
+
const currentMouseAngle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI;
|
|
1033
|
+
const angleDiff = currentMouseAngle - startRotationMouseAngle;
|
|
1034
|
+
|
|
1035
|
+
imageRotation = startImageRotation + angleDiff;
|
|
1036
|
+
imageRotation = imageRotation % 360;
|
|
1037
|
+
if (imageRotation < 0) imageRotation += 360;
|
|
1038
|
+
|
|
1039
|
+
// Apply rotation transform
|
|
1040
|
+
selectedImage.setAttribute('transform', `rotate(${imageRotation}, ${centerX}, ${centerY})`);
|
|
1041
|
+
|
|
1042
|
+
// Update data attribute for arrow attachment
|
|
1043
|
+
selectedImage.setAttribute('data-shape-rotation', imageRotation);
|
|
1044
|
+
|
|
1045
|
+
// Update attached arrows during rotation
|
|
1046
|
+
updateArrowsForShape(selectedImage);
|
|
1047
|
+
|
|
1048
|
+
// Update the selection outline and anchors
|
|
1049
|
+
removeSelectionOutline();
|
|
1050
|
+
addSelectionOutline();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function stopInteracting() {
|
|
1054
|
+
// Store transform data before stopping interaction
|
|
1055
|
+
if (selectedImage && (isDragging || isRotatingImage || currentAnchor)) {
|
|
1056
|
+
const newPos = {
|
|
1057
|
+
x: parseFloat(selectedImage.getAttribute('x')),
|
|
1058
|
+
y: parseFloat(selectedImage.getAttribute('y')),
|
|
1059
|
+
width: parseFloat(selectedImage.getAttribute('width')),
|
|
1060
|
+
height: parseFloat(selectedImage.getAttribute('height')),
|
|
1061
|
+
rotation: imageRotation
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
// Get the original rotation from the stored start rotation or current rotation
|
|
1065
|
+
let originalRotation = imageRotation;
|
|
1066
|
+
if (isRotatingImage && startImageRotation !== null) {
|
|
1067
|
+
originalRotation = startImageRotation;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const oldPos = {
|
|
1071
|
+
x: originalX,
|
|
1072
|
+
y: originalY,
|
|
1073
|
+
width: originalWidth,
|
|
1074
|
+
height: originalHeight,
|
|
1075
|
+
rotation: originalRotation
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// Find the ImageShape wrapper for frame tracking
|
|
1079
|
+
let imageShape = null;
|
|
1080
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1081
|
+
imageShape = shapes.find(shape => shape.shapeName === 'image' && shape.element === selectedImage);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Add frame information for undo tracking
|
|
1085
|
+
const oldPosWithFrame = {
|
|
1086
|
+
...oldPos,
|
|
1087
|
+
parentFrame: draggedShapeInitialFrameImage
|
|
1088
|
+
};
|
|
1089
|
+
const newPosWithFrame = {
|
|
1090
|
+
...newPos,
|
|
1091
|
+
parentFrame: imageShape ? imageShape.parentFrame : null
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// Only push transform action if something actually changed
|
|
1095
|
+
const stateChanged = newPos.x !== oldPos.x || newPos.y !== oldPos.y ||
|
|
1096
|
+
newPos.width !== oldPos.width || newPos.height !== oldPos.height ||
|
|
1097
|
+
newPos.rotation !== oldPos.rotation;
|
|
1098
|
+
const frameChanged = oldPosWithFrame.parentFrame !== newPosWithFrame.parentFrame;
|
|
1099
|
+
|
|
1100
|
+
if (stateChanged || frameChanged) {
|
|
1101
|
+
pushTransformAction({
|
|
1102
|
+
type: 'image',
|
|
1103
|
+
element: selectedImage,
|
|
1104
|
+
restore: (pos) => {
|
|
1105
|
+
selectedImage.setAttribute('x', pos.x);
|
|
1106
|
+
selectedImage.setAttribute('y', pos.y);
|
|
1107
|
+
selectedImage.setAttribute('width', pos.width);
|
|
1108
|
+
selectedImage.setAttribute('height', pos.height);
|
|
1109
|
+
const centerX = pos.x + pos.width / 2;
|
|
1110
|
+
const centerY = pos.y + pos.height / 2;
|
|
1111
|
+
selectedImage.setAttribute('transform', `rotate(${pos.rotation}, ${centerX}, ${centerY})`);
|
|
1112
|
+
imageRotation = pos.rotation;
|
|
1113
|
+
|
|
1114
|
+
// Update selection outline if image is selected
|
|
1115
|
+
if (selectedImage) {
|
|
1116
|
+
removeSelectionOutline();
|
|
1117
|
+
addSelectionOutline();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Update data attributes for arrow attachment consistency
|
|
1121
|
+
selectedImage.setAttribute('data-shape-x', pos.x);
|
|
1122
|
+
selectedImage.setAttribute('data-shape-y', pos.y);
|
|
1123
|
+
selectedImage.setAttribute('data-shape-width', pos.width);
|
|
1124
|
+
selectedImage.setAttribute('data-shape-height', pos.height);
|
|
1125
|
+
selectedImage.setAttribute('data-shape-rotation', pos.rotation);
|
|
1126
|
+
|
|
1127
|
+
// Update attached arrows
|
|
1128
|
+
updateArrowsForShape(selectedImage);
|
|
1129
|
+
}
|
|
1130
|
+
}, oldPosWithFrame, newPosWithFrame);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Handle frame containment changes after drag
|
|
1134
|
+
if (isDragging && imageShape) {
|
|
1135
|
+
const finalFrame = hoveredFrameImage;
|
|
1136
|
+
|
|
1137
|
+
// If shape moved to a different frame
|
|
1138
|
+
if (draggedShapeInitialFrameImage !== finalFrame) {
|
|
1139
|
+
// Remove from initial frame
|
|
1140
|
+
if (draggedShapeInitialFrameImage) {
|
|
1141
|
+
draggedShapeInitialFrameImage.removeShapeFromFrame(imageShape);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Add to new frame
|
|
1145
|
+
if (finalFrame) {
|
|
1146
|
+
finalFrame.addShapeToFrame(imageShape);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Track the frame change for undo
|
|
1150
|
+
if (frameChanged) {
|
|
1151
|
+
pushFrameAttachmentAction(finalFrame || draggedShapeInitialFrameImage, imageShape,
|
|
1152
|
+
finalFrame ? 'attach' : 'detach', draggedShapeInitialFrameImage);
|
|
1153
|
+
}
|
|
1154
|
+
} else if (draggedShapeInitialFrameImage) {
|
|
1155
|
+
// Shape stayed in same frame, restore clipping
|
|
1156
|
+
draggedShapeInitialFrameImage.restoreToFrame(imageShape);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
draggedShapeInitialFrameImage = null;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Clear frame highlighting
|
|
1164
|
+
if (hoveredFrameImage) {
|
|
1165
|
+
hoveredFrameImage.removeHighlight();
|
|
1166
|
+
hoveredFrameImage = null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
isDragging = false;
|
|
1170
|
+
isRotatingImage = false;
|
|
1171
|
+
svg.removeEventListener('mousemove', dragImage);
|
|
1172
|
+
svg.removeEventListener('mousemove', resizeImage);
|
|
1173
|
+
svg.removeEventListener('mousemove', rotateImage);
|
|
1174
|
+
currentAnchor = null;
|
|
1175
|
+
startRotationMouseAngle = null;
|
|
1176
|
+
startImageRotation = null;
|
|
1177
|
+
|
|
1178
|
+
// Update originalX, originalY, originalWidth and originalHeight after dragging/resizing is complete
|
|
1179
|
+
if (selectedImage) {
|
|
1180
|
+
originalX = parseFloat(selectedImage.getAttribute('x'));
|
|
1181
|
+
originalY = parseFloat(selectedImage.getAttribute('y'));
|
|
1182
|
+
originalWidth = parseFloat(selectedImage.getAttribute('width'));
|
|
1183
|
+
originalHeight = parseFloat(selectedImage.getAttribute('height'));
|
|
1184
|
+
|
|
1185
|
+
// Update the current rotation from the image transform
|
|
1186
|
+
const transform = selectedImage.getAttribute('transform');
|
|
1187
|
+
if (transform) {
|
|
1188
|
+
const rotateMatch = transform.match(/rotate\(([^,]+)/);
|
|
1189
|
+
if (rotateMatch) {
|
|
1190
|
+
imageRotation = parseFloat(rotateMatch[1]);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Update data attributes for arrow attachment consistency
|
|
1195
|
+
selectedImage.setAttribute('data-shape-x', originalX);
|
|
1196
|
+
selectedImage.setAttribute('data-shape-y', originalY);
|
|
1197
|
+
selectedImage.setAttribute('data-shape-width', originalWidth);
|
|
1198
|
+
selectedImage.setAttribute('data-shape-height', originalHeight);
|
|
1199
|
+
selectedImage.setAttribute('data-shape-rotation', imageRotation);
|
|
1200
|
+
|
|
1201
|
+
// Update attached arrows after interaction ends
|
|
1202
|
+
updateArrowsForShape(selectedImage);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Add delete functionality for images
|
|
1207
|
+
function deleteCurrentImage() {
|
|
1208
|
+
if (selectedImage) {
|
|
1209
|
+
// Find the ImageShape wrapper
|
|
1210
|
+
let imageShape = (typeof shapes !== 'undefined' && Array.isArray(shapes))
|
|
1211
|
+
? shapes.find(s => s.shapeName === 'image' && s.element === selectedImage)
|
|
1212
|
+
: null;
|
|
1213
|
+
|
|
1214
|
+
// Abort any in-progress upload
|
|
1215
|
+
if (imageShape?.uploadAbortController) {
|
|
1216
|
+
imageShape.uploadAbortController.abort();
|
|
1217
|
+
imageShape.removeUploadIndicator();
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Release image bytes from room limit
|
|
1221
|
+
const freedBytes = selectedImage.__fileSize || 0;
|
|
1222
|
+
window.__roomImageBytesUsed = Math.max(0, (window.__roomImageBytesUsed || 0) - freedBytes);
|
|
1223
|
+
|
|
1224
|
+
// If image is hosted on Cloudinary, delete it from storage
|
|
1225
|
+
const imgHref = selectedImage.getAttribute('href') || selectedImage.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || '';
|
|
1226
|
+
if (imgHref.includes('cloudinary.com') || imgHref.includes('res.cloudinary')) {
|
|
1227
|
+
const match = imgHref.match(/\/upload\/(?:v\d+\/)?(lixsketch\/.+?)(?:\.\w+)?$/);
|
|
1228
|
+
if (match) {
|
|
1229
|
+
const publicId = match[1];
|
|
1230
|
+
const workerUrl = window.__WORKER_URL;
|
|
1231
|
+
if (workerUrl) {
|
|
1232
|
+
fetch(`${workerUrl}/api/images/delete`, {
|
|
1233
|
+
method: 'DELETE',
|
|
1234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1235
|
+
body: JSON.stringify({ publicId }),
|
|
1236
|
+
}).catch(err => console.warn('[ImageTool] Cloudinary cleanup failed:', err));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (imageShape) {
|
|
1242
|
+
const idx = shapes.indexOf(imageShape);
|
|
1243
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
1244
|
+
|
|
1245
|
+
// Remove the group (which contains the image)
|
|
1246
|
+
if (imageShape.group && imageShape.group.parentNode) {
|
|
1247
|
+
imageShape.group.parentNode.removeChild(imageShape.group);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Fallback: if no ImageShape wrapper found, remove the image directly
|
|
1252
|
+
if (!imageShape && selectedImage.parentNode) {
|
|
1253
|
+
selectedImage.parentNode.removeChild(selectedImage);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Clean up any arrow attachments before deleting
|
|
1257
|
+
if (typeof cleanupAttachments === 'function') {
|
|
1258
|
+
cleanupAttachments(selectedImage);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Push delete action for undo
|
|
1262
|
+
if (imageShape) {
|
|
1263
|
+
pushDeleteAction(imageShape);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Clean up selection
|
|
1267
|
+
removeSelectionOutline();
|
|
1268
|
+
selectedImage = null;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
document.addEventListener('keydown', (e) => {
|
|
1273
|
+
if (e.key === 'Delete' && selectedImage) {
|
|
1274
|
+
deleteCurrentImage();
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// Expose upload pipeline globally so generated/pasted images can use it
|
|
1279
|
+
window.uploadImageToCloudinary = uploadImageToCloudinary;
|
|
1280
|
+
|
|
1281
|
+
// Window bridge: allow React UI to trigger the file picker
|
|
1282
|
+
window.openImageFilePicker = function() {
|
|
1283
|
+
isImageToolActive = true;
|
|
1284
|
+
const fileInput = document.createElement('input');
|
|
1285
|
+
fileInput.type = 'file';
|
|
1286
|
+
fileInput.accept = 'image/*';
|
|
1287
|
+
fileInput.style.display = 'none';
|
|
1288
|
+
document.body.appendChild(fileInput);
|
|
1289
|
+
|
|
1290
|
+
let fileSelected = false;
|
|
1291
|
+
|
|
1292
|
+
fileInput.addEventListener('change', (event) => {
|
|
1293
|
+
const file = event.target.files[0];
|
|
1294
|
+
if (file) {
|
|
1295
|
+
fileSelected = true;
|
|
1296
|
+
handleImageUpload(file);
|
|
1297
|
+
}
|
|
1298
|
+
document.body.removeChild(fileInput);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Detect cancel: when focus returns to window without a file being selected
|
|
1302
|
+
const onFocus = () => {
|
|
1303
|
+
window.removeEventListener('focus', onFocus);
|
|
1304
|
+
// Delay to let 'change' fire first if a file was selected
|
|
1305
|
+
setTimeout(() => {
|
|
1306
|
+
if (!fileSelected) {
|
|
1307
|
+
// User cancelled — switch back to select tool
|
|
1308
|
+
isImageToolActive = false;
|
|
1309
|
+
if (window.__sketchEngine?.setActiveTool) {
|
|
1310
|
+
window.__sketchEngine.setActiveTool('select');
|
|
1311
|
+
}
|
|
1312
|
+
if (fileInput.parentNode) {
|
|
1313
|
+
document.body.removeChild(fileInput);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}, 300);
|
|
1317
|
+
};
|
|
1318
|
+
window.addEventListener('focus', onFocus);
|
|
1319
|
+
|
|
1320
|
+
fileInput.click();
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
export { handleMouseDownImage, handleMouseMoveImage, handleMouseUpImage };
|