@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/fonts/fonts.css +29 -0
  4. package/fonts/lixCode.ttf +0 -0
  5. package/fonts/lixDefault.ttf +0 -0
  6. package/fonts/lixDocs.ttf +0 -0
  7. package/fonts/lixFancy.ttf +0 -0
  8. package/fonts/lixFont.woff2 +0 -0
  9. package/package.json +49 -0
  10. package/src/SketchEngine.js +473 -0
  11. package/src/core/AIRenderer.js +1390 -0
  12. package/src/core/CopyPaste.js +655 -0
  13. package/src/core/EraserTrail.js +234 -0
  14. package/src/core/EventDispatcher.js +371 -0
  15. package/src/core/GraphEngine.js +150 -0
  16. package/src/core/GraphMathParser.js +231 -0
  17. package/src/core/GraphRenderer.js +255 -0
  18. package/src/core/LayerOrder.js +91 -0
  19. package/src/core/LixScriptParser.js +1299 -0
  20. package/src/core/MermaidFlowchartRenderer.js +475 -0
  21. package/src/core/MermaidSequenceParser.js +197 -0
  22. package/src/core/MermaidSequenceRenderer.js +479 -0
  23. package/src/core/ResizeCode.js +175 -0
  24. package/src/core/ResizeShapes.js +318 -0
  25. package/src/core/SceneSerializer.js +778 -0
  26. package/src/core/Selection.js +1861 -0
  27. package/src/core/SnapGuides.js +273 -0
  28. package/src/core/UndoRedo.js +1358 -0
  29. package/src/core/ZoomPan.js +258 -0
  30. package/src/core/ai-system-prompt.js +663 -0
  31. package/src/index.js +69 -0
  32. package/src/shapes/Arrow.js +1979 -0
  33. package/src/shapes/Circle.js +751 -0
  34. package/src/shapes/CodeShape.js +244 -0
  35. package/src/shapes/Frame.js +1460 -0
  36. package/src/shapes/FreehandStroke.js +724 -0
  37. package/src/shapes/IconShape.js +265 -0
  38. package/src/shapes/ImageShape.js +270 -0
  39. package/src/shapes/Line.js +738 -0
  40. package/src/shapes/Rectangle.js +794 -0
  41. package/src/shapes/TextShape.js +225 -0
  42. package/src/tools/arrowTool.js +581 -0
  43. package/src/tools/circleTool.js +619 -0
  44. package/src/tools/codeTool.js +2103 -0
  45. package/src/tools/eraserTool.js +131 -0
  46. package/src/tools/frameTool.js +241 -0
  47. package/src/tools/freehandTool.js +620 -0
  48. package/src/tools/iconTool.js +1344 -0
  49. package/src/tools/imageTool.js +1323 -0
  50. package/src/tools/laserTool.js +317 -0
  51. package/src/tools/lineTool.js +502 -0
  52. package/src/tools/rectangleTool.js +544 -0
  53. package/src/tools/textTool.js +1823 -0
  54. package/src/utils/imageCompressor.js +107 -0
@@ -0,0 +1,620 @@
1
+ /* eslint-disable */
2
+ // Freehand tool event handlers - extracted from canvasStroke.js
3
+ import { pushCreateAction, pushDeleteAction, pushOptionsChangeAction, pushTransformAction, pushFrameAttachmentAction } from '../core/UndoRedo.js';
4
+ import { updateAttachedArrows as updateArrowsForShape, cleanupAttachments } from './arrowTool.js';
5
+ import { calculateSnap, clearSnapGuides } from '../core/SnapGuides.js';
6
+
7
+
8
+ const strokeColors = document.querySelectorAll(".strokeColors span");
9
+ const strokeThicknesses = document.querySelectorAll(".strokeThickness span");
10
+ const strokeStyles = document.querySelectorAll(".strokeStyleSpan");
11
+ const strokeTapers = document.querySelectorAll(".strokeTaperSpan");
12
+ const strokeRoughnesses = document.querySelectorAll(".strokeRoughnessSpan");
13
+ let strokeColor = "#fff";
14
+ let strokeThickness = 2;
15
+ let strokeStyleValue = "solid";
16
+ let strokeThinning = 0;
17
+ let strokeRoughnessValue = "smooth";
18
+ let points = [];
19
+ let isDrawingStroke = false;
20
+ let currentStroke = null;
21
+ let strokeOpacity = 1;
22
+
23
+
24
+ let isDraggingStroke = false;
25
+ let isResizingStroke = false;
26
+ let isRotatingStroke = false;
27
+ let dragOldPosStroke = null;
28
+ let resizingAnchorIndex = null;
29
+ let startRotationMouseAngle = null;
30
+ let startShapeRotation = null;
31
+ let startX, startY;
32
+
33
+ // Frame attachment variables
34
+ let draggedShapeInitialFrameStroke = null;
35
+ let hoveredFrameStroke = null;
36
+
37
+ // Enhanced mouse tracking with better point collection
38
+ let lastPoint = null;
39
+ let lastTime = 0;
40
+ const minDistance = 2; // Minimum distance between points
41
+ const maxDistance = 15; // Maximum distance for interpolation
42
+ let isdraggingOpacity = false;
43
+
44
+
45
+ function getSvgCoordinates(event) {
46
+ const rect = svg.getBoundingClientRect();
47
+ const scaleX = currentViewBox.width / rect.width;
48
+ const scaleY = currentViewBox.height / rect.height;
49
+
50
+ const svgX = currentViewBox.x + (event.clientX - rect.left) * scaleX;
51
+ const svgY = currentViewBox.y + (event.clientY - rect.top) * scaleY;
52
+
53
+ return { x: svgX, y: svgY };
54
+ }
55
+
56
+ function getSvgPathFromStroke(stroke) {
57
+ if (!stroke.length) return '';
58
+
59
+ // Use more sophisticated curve fitting
60
+ const pathData = [];
61
+ pathData.push('M', stroke[0][0], stroke[0][1]);
62
+
63
+ for (let i = 1; i < stroke.length - 1; i++) {
64
+ const curr = stroke[i];
65
+ const next = stroke[i + 1];
66
+
67
+ // Calculate control points for smoother curves
68
+ const cpX = curr[0] + (next[0] - curr[0]) * 0.5;
69
+ const cpY = curr[1] + (next[1] - curr[1]) * 0.5;
70
+
71
+ pathData.push('Q', curr[0], curr[1], cpX, cpY);
72
+ }
73
+
74
+ // Add final point
75
+ if (stroke.length > 1) {
76
+ const lastPoint = stroke[stroke.length - 1];
77
+ pathData.push('L', lastPoint[0], lastPoint[1]);
78
+ }
79
+
80
+ return pathData.join(' ');
81
+ }
82
+
83
+ // Delete functionality
84
+ function deleteCurrentShape() {
85
+ if (currentShape && currentShape.shapeName === 'freehandStroke') {
86
+ const idx = shapes.indexOf(currentShape);
87
+ if (idx !== -1) shapes.splice(idx, 1);
88
+ if (currentShape.group.parentNode) {
89
+ currentShape.group.parentNode.removeChild(currentShape.group);
90
+ }
91
+ pushDeleteAction(currentShape);
92
+ currentShape = null;
93
+ disableAllSideBars();
94
+ }
95
+ }
96
+
97
+ document.addEventListener('keydown', (e) => {
98
+ if (e.key === 'Delete' && currentShape && currentShape.shapeName === 'freehandStroke') {
99
+ deleteCurrentShape();
100
+ }
101
+ });
102
+
103
+ // Event handlers
104
+ function handleMouseDown(e) {
105
+ const { x, y } = getSvgCoordinates(e);
106
+
107
+ if (isPaintToolActive) {
108
+ isDrawingStroke = true;
109
+ const pressure = e.pressure || 0.5;
110
+ points = [[x, y, pressure]];
111
+ lastPoint = [x, y, pressure];
112
+ lastTime = Date.now();
113
+
114
+ currentStroke = new FreehandStroke(points, {
115
+ stroke: strokeColor,
116
+ strokeWidth: strokeThickness
117
+ });
118
+
119
+ shapes.push(currentStroke);
120
+ currentShape = currentStroke;
121
+ } else if (isSelectionToolActive) {
122
+ let clickedOnShape = false;
123
+
124
+ // Check if clicking on current selected stroke
125
+ if (currentShape && currentShape.shapeName === 'freehandStroke' && currentShape.isSelected) {
126
+ const anchorInfo = currentShape.isNearAnchor(x, y);
127
+ if (anchorInfo) {
128
+ if (anchorInfo.type === 'resize') {
129
+ isResizingStroke = true;
130
+ resizingAnchorIndex = anchorInfo.index;
131
+ dragOldPosStroke = cloneStrokeData(currentShape);
132
+ } else if (anchorInfo.type === 'rotate') {
133
+ isRotatingStroke = true;
134
+ const centerX = currentShape.boundingBox.x + currentShape.boundingBox.width / 2;
135
+ const centerY = currentShape.boundingBox.y + currentShape.boundingBox.height / 2;
136
+ startRotationMouseAngle = Math.atan2(y - centerY, x - centerX) * 180 / Math.PI;
137
+ startShapeRotation = currentShape.rotation;
138
+ dragOldPosStroke = cloneStrokeData(currentShape);
139
+ }
140
+ clickedOnShape = true;
141
+ } else if (currentShape.contains(x, y)) {
142
+ isDraggingStroke = true;
143
+ dragOldPosStroke = cloneStrokeData(currentShape);
144
+
145
+ // Store initial frame state
146
+ draggedShapeInitialFrameStroke = currentShape.parentFrame || null;
147
+
148
+ // Temporarily remove from frame clipping if dragging
149
+ if (currentShape.parentFrame) {
150
+ currentShape.parentFrame.temporarilyRemoveFromFrame(currentShape);
151
+ }
152
+
153
+ startX = x;
154
+ startY = y;
155
+ clickedOnShape = true;
156
+ }
157
+ }
158
+
159
+ // If not clicking on selected shape, check for other shapes
160
+ if (!clickedOnShape) {
161
+ let shapeToSelect = null;
162
+ for (let i = shapes.length - 1; i >= 0; i--) {
163
+ const shape = shapes[i];
164
+ if (shape instanceof FreehandStroke && shape.contains(x, y)) {
165
+ shapeToSelect = shape;
166
+ break;
167
+ }
168
+ }
169
+
170
+ if (currentShape && currentShape !== shapeToSelect) {
171
+ currentShape.deselectStroke();
172
+ currentShape = null;
173
+ }
174
+
175
+ if (shapeToSelect) {
176
+ currentShape = shapeToSelect;
177
+ currentShape.selectStroke(); // This will show the sidebar
178
+
179
+ const anchorInfo = currentShape.isNearAnchor(x, y);
180
+ if (anchorInfo) {
181
+ if (anchorInfo.type === 'resize') {
182
+ isResizingStroke = true;
183
+ resizingAnchorIndex = anchorInfo.index;
184
+ dragOldPosStroke = cloneStrokeData(currentShape);
185
+ } else if (anchorInfo.type === 'rotate') {
186
+ isRotatingStroke = true;
187
+ const centerX = currentShape.boundingBox.x + currentShape.boundingBox.width / 2;
188
+ const centerY = currentShape.boundingBox.y + currentShape.boundingBox.height / 2;
189
+ startRotationMouseAngle = Math.atan2(y - centerY, x - centerX) * 180 / Math.PI;
190
+ startShapeRotation = currentShape.rotation;
191
+ dragOldPosStroke = cloneStrokeData(currentShape);
192
+ }
193
+ } else {
194
+ isDraggingStroke = true;
195
+ dragOldPosStroke = cloneStrokeData(currentShape);
196
+
197
+ // Store initial frame state
198
+ draggedShapeInitialFrameStroke = currentShape.parentFrame || null;
199
+
200
+ // Temporarily remove from frame clipping if dragging
201
+ if (currentShape.parentFrame) {
202
+ currentShape.parentFrame.temporarilyRemoveFromFrame(currentShape);
203
+ }
204
+
205
+ startX = x;
206
+ startY = y;
207
+ }
208
+ clickedOnShape = true;
209
+ }
210
+ }
211
+
212
+ if (!clickedOnShape && currentShape) {
213
+ currentShape.deselectStroke();
214
+ currentShape = null;
215
+ disableAllSideBars(); // Hide sidebar when deselecting
216
+ }
217
+ }
218
+ }
219
+
220
+ function handleMouseMove(e) {
221
+ let { x, y } = getSvgCoordinates(e);
222
+ const currentTime = Date.now();
223
+
224
+ // Keep lastMousePos in screen coordinates for other functions
225
+ const svgRect = svg.getBoundingClientRect();
226
+ lastMousePos = {
227
+ x: e.clientX - svgRect.left,
228
+ y: e.clientY - svgRect.top
229
+ };
230
+
231
+ if (isDrawingStroke && isPaintToolActive) {
232
+ const pressure = e.pressure || 0.5;
233
+
234
+ // Shift key constrains to straight line from start point
235
+ if (e.shiftKey && points.length > 0) {
236
+ const startX = points[0][0], startY = points[0][1];
237
+ const dx = x - startX, dy = y - startY;
238
+ const angle = Math.atan2(dy, dx);
239
+ const snapAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4);
240
+ const dist = Math.sqrt(dx * dx + dy * dy);
241
+ x = startX + dist * Math.cos(snapAngle);
242
+ y = startY + dist * Math.sin(snapAngle);
243
+ // Reset points to just start + current for straight line
244
+ points = [points[0], [x, y, pressure]];
245
+ lastPoint = [x, y, pressure];
246
+ currentStroke.points = points;
247
+ currentStroke.draw();
248
+ return;
249
+ }
250
+
251
+ if (lastPoint) {
252
+ const dx = x - lastPoint[0];
253
+ const dy = y - lastPoint[1];
254
+ const distance = Math.sqrt(dx * dx + dy * dy);
255
+
256
+ // Only add point if it's far enough from the last one
257
+ if (distance >= minDistance) {
258
+ // If distance is too large, interpolate points
259
+ if (distance > maxDistance) {
260
+ const steps = Math.ceil(distance / maxDistance);
261
+ for (let i = 1; i < steps; i++) {
262
+ const t = i / steps;
263
+ const interpX = lastPoint[0] + dx * t;
264
+ const interpY = lastPoint[1] + dy * t;
265
+ const interpPressure = lastPoint[2] + (pressure - lastPoint[2]) * t;
266
+ points.push([interpX, interpY, interpPressure]);
267
+ }
268
+ }
269
+
270
+ // Calculate velocity-based pressure
271
+ const timeDelta = currentTime - lastTime;
272
+ const velocity = distance / Math.max(timeDelta, 1);
273
+ const velocityPressure = Math.min(1, Math.max(0.1, 1 - velocity * 0.02));
274
+ const finalPressure = (pressure + velocityPressure) * 0.5;
275
+
276
+ points.push([x, y, finalPressure]);
277
+ currentStroke.points = points;
278
+ currentStroke.draw();
279
+
280
+ lastPoint = [x, y, finalPressure];
281
+ lastTime = currentTime;
282
+ }
283
+ } else {
284
+ lastPoint = [x, y, pressure];
285
+ lastTime = currentTime;
286
+ }
287
+
288
+ // Check for frame containment while drawing (but don't apply clipping yet)
289
+ shapes.forEach(frame => {
290
+ if (frame.shapeName === 'frame') {
291
+ if (frame.isShapeInFrame(currentStroke)) {
292
+ frame.highlightFrame();
293
+ hoveredFrameStroke = frame;
294
+ } else if (hoveredFrameStroke === frame) {
295
+ frame.removeHighlight();
296
+ hoveredFrameStroke = null;
297
+ }
298
+ }
299
+ });
300
+ } else if (isDraggingStroke && currentShape && currentShape.isSelected) {
301
+ const dx = x - startX;
302
+ const dy = y - startY;
303
+ currentShape.move(dx, dy);
304
+ startX = x;
305
+ startY = y;
306
+
307
+ // Snap guides
308
+ if (window.__sketchStoreApi && window.__sketchStoreApi.getState().snapToObjects) {
309
+ const snap = calculateSnap(currentShape, e.shiftKey, e.clientX, e.clientY);
310
+ if (snap.dx || snap.dy) {
311
+ currentShape.move(snap.dx, snap.dy);
312
+ }
313
+ } else {
314
+ clearSnapGuides();
315
+ }
316
+ } else if (isResizingStroke && currentShape && currentShape.isSelected) {
317
+ currentShape.updatePosition(resizingAnchorIndex, x, y);
318
+ } else if (isRotatingStroke && currentShape && currentShape.isSelected) {
319
+ const centerX = currentShape.boundingBox.x + currentShape.boundingBox.width / 2;
320
+ const centerY = currentShape.boundingBox.y + currentShape.boundingBox.height / 2;
321
+ const currentMouseAngle = Math.atan2(y - centerY, x - centerX) * 180 / Math.PI;
322
+ const angleDiff = currentMouseAngle - startRotationMouseAngle;
323
+ currentShape.rotate(startShapeRotation + angleDiff);
324
+ currentShape.draw();
325
+ }
326
+ }
327
+
328
+ function handleMouseUp(e) {
329
+ if (isDrawingStroke) {
330
+ isDrawingStroke = false;
331
+ lastPoint = null;
332
+
333
+ // Final smoothing pass after drawing is complete
334
+ if (currentStroke && currentStroke.points.length >= 2) {
335
+ currentStroke.draw(); // Redraw with final smoothing
336
+ pushCreateAction(currentStroke);
337
+
338
+ // Check for frame containment and track attachment
339
+ const finalFrame = hoveredFrameStroke;
340
+ if (finalFrame) {
341
+ finalFrame.addShapeToFrame(currentStroke);
342
+ // Track the attachment for undo
343
+ pushFrameAttachmentAction(finalFrame, currentStroke, 'attach', null);
344
+ }
345
+ } else if (currentStroke) {
346
+ // Remove strokes that are too small
347
+ shapes.pop();
348
+ if (currentStroke.group.parentNode) {
349
+ currentStroke.group.parentNode.removeChild(currentStroke.group);
350
+ }
351
+ }
352
+
353
+ // Clear frame highlighting
354
+ if (hoveredFrameStroke) {
355
+ hoveredFrameStroke.removeHighlight();
356
+ hoveredFrameStroke = null;
357
+ }
358
+
359
+ currentStroke = null;
360
+ }
361
+
362
+ if ((isDraggingStroke || isResizingStroke || isRotatingStroke) && dragOldPosStroke && currentShape) {
363
+ const newPos = cloneStrokeData(currentShape);
364
+ const stateChanged =
365
+ JSON.stringify(dragOldPosStroke.points) !== JSON.stringify(newPos.points) ||
366
+ dragOldPosStroke.rotation !== newPos.rotation;
367
+
368
+ const oldPos = {
369
+ ...dragOldPosStroke,
370
+ parentFrame: draggedShapeInitialFrameStroke
371
+ };
372
+ const newPosForUndo = {
373
+ ...newPos,
374
+ parentFrame: currentShape.parentFrame
375
+ };
376
+
377
+ const frameChanged = oldPos.parentFrame !== newPosForUndo.parentFrame;
378
+
379
+ if (stateChanged || frameChanged) {
380
+ pushTransformAction(currentShape, oldPos, newPosForUndo);
381
+ }
382
+
383
+ // Handle frame containment changes after drag
384
+ if (isDraggingStroke) {
385
+ const finalFrame = hoveredFrameStroke;
386
+
387
+ // If shape moved to a different frame
388
+ if (draggedShapeInitialFrameStroke !== finalFrame) {
389
+ // Remove from initial frame
390
+ if (draggedShapeInitialFrameStroke) {
391
+ draggedShapeInitialFrameStroke.removeShapeFromFrame(currentShape);
392
+ }
393
+
394
+ // Add to new frame
395
+ if (finalFrame) {
396
+ finalFrame.addShapeToFrame(currentShape);
397
+ }
398
+
399
+ // Track the frame change for undo
400
+ if (frameChanged) {
401
+ pushFrameAttachmentAction(finalFrame || draggedShapeInitialFrameStroke, currentShape,
402
+ finalFrame ? 'attach' : 'detach', draggedShapeInitialFrameStroke);
403
+ }
404
+ } else if (draggedShapeInitialFrameStroke) {
405
+ // Shape stayed in same frame, restore clipping
406
+ draggedShapeInitialFrameStroke.restoreToFrame(currentShape);
407
+ }
408
+ }
409
+
410
+ dragOldPosStroke = null;
411
+ draggedShapeInitialFrameStroke = null;
412
+ }
413
+
414
+ // Clear frame highlighting
415
+ if (hoveredFrameStroke) {
416
+ hoveredFrameStroke.removeHighlight();
417
+ hoveredFrameStroke = null;
418
+ }
419
+
420
+ // Bake accumulated move offset into point coordinates
421
+ if (currentShape && typeof currentShape.finalizeMove === 'function') {
422
+ currentShape.finalizeMove();
423
+ }
424
+ clearSnapGuides();
425
+ isDraggingStroke = false;
426
+ isResizingStroke = false;
427
+ isRotatingStroke = false;
428
+ resizingAnchorIndex = null;
429
+ startRotationMouseAngle = null;
430
+ startShapeRotation = null;
431
+ svg.style.cursor = 'default';
432
+ }
433
+
434
+ // Color and thickness selection
435
+ strokeColors.forEach(span => {
436
+ span.addEventListener("click", (event) => {
437
+ strokeColors.forEach(el => el.classList.remove("selected"));
438
+ span.classList.add("selected");
439
+
440
+ if (currentShape instanceof FreehandStroke && currentShape.isSelected) {
441
+ const oldOptions = {...currentShape.options};
442
+ currentShape.options.stroke = span.getAttribute("data-id");
443
+ currentShape.draw();
444
+ pushOptionsChangeAction(currentShape, oldOptions);
445
+ } else {
446
+ strokeColor = span.getAttribute("data-id");
447
+ }
448
+ });
449
+ });
450
+
451
+ strokeThicknesses.forEach(span => {
452
+ span.addEventListener("click", (event) => {
453
+ strokeThicknesses.forEach(el => el.classList.remove("selected"));
454
+ span.classList.add("selected");
455
+
456
+ if (currentShape instanceof FreehandStroke && currentShape.isSelected) {
457
+ const oldOptions = {...currentShape.options};
458
+ currentShape.options.strokeWidth = parseInt(span.getAttribute("data-id"));
459
+ currentShape.draw();
460
+ pushOptionsChangeAction(currentShape, oldOptions);
461
+ } else {
462
+ strokeThickness = parseInt(span.getAttribute("data-id"));
463
+ }
464
+ });
465
+ });
466
+
467
+
468
+ strokeStyles.forEach(span => {
469
+ span.addEventListener("click", () => {
470
+ strokeStyles.forEach(el => el.classList.remove("selected"));
471
+ span.classList.add("selected");
472
+ const val = span.getAttribute("data-id");
473
+ if (currentShape instanceof FreehandStroke && currentShape.isSelected) {
474
+ const oldOptions = {...currentShape.options};
475
+ currentShape.options.strokeStyle = val;
476
+ currentShape.draw();
477
+ pushOptionsChangeAction(currentShape, oldOptions);
478
+ } else {
479
+ strokeStyleValue = val;
480
+ }
481
+ });
482
+ });
483
+
484
+ strokeTapers.forEach(span => {
485
+ span.addEventListener("click", () => {
486
+ strokeTapers.forEach(el => el.classList.remove("selected"));
487
+ span.classList.add("selected");
488
+ const val = parseFloat(span.getAttribute("data-id"));
489
+ if (currentShape instanceof FreehandStroke && currentShape.isSelected) {
490
+ const oldOptions = {...currentShape.options};
491
+ currentShape.options.thinning = val;
492
+ currentShape.draw();
493
+ pushOptionsChangeAction(currentShape, oldOptions);
494
+ } else {
495
+ strokeThinning = val;
496
+ }
497
+ });
498
+ });
499
+
500
+ strokeRoughnesses.forEach(span => {
501
+ span.addEventListener("click", () => {
502
+ strokeRoughnesses.forEach(el => el.classList.remove("selected"));
503
+ span.classList.add("selected");
504
+ const val = span.getAttribute("data-id");
505
+ if (currentShape instanceof FreehandStroke && currentShape.isSelected) {
506
+ const oldOptions = {...currentShape.options};
507
+ currentShape.options.roughness = val;
508
+ currentShape.draw();
509
+ pushOptionsChangeAction(currentShape, oldOptions);
510
+ } else {
511
+ strokeRoughnessValue = val;
512
+ }
513
+ });
514
+ });
515
+
516
+ document.getElementById("strokeOpacity")?.addEventListener("mousedown", (event) => {
517
+ isdraggingOpacity = true;
518
+ });
519
+
520
+ document.getElementById("strokeOpacity")?.addEventListener("mousemove", (event) => {
521
+ if(isdraggingOpacity)
522
+ {
523
+ const slider = document.getElementById("strokeOpacity");
524
+ const rect = slider.getBoundingClientRect();
525
+ const mouseX = event.clientX - rect.left;
526
+ const percent = Math.max(0, Math.min(100, (mouseX / rect.width) * 100));
527
+ document.getElementById("opacityContainerValue").textContent = percent.toFixed(0);
528
+ const opacity = percent / 100;
529
+ if (currentShape instanceof FreehandStroke && currentShape.isSelected) {
530
+ const oldOptions = {...currentShape.options};
531
+ currentShape.options.strokeOpacity = opacity;
532
+ currentShape.draw();
533
+ pushOptionsChangeAction(currentShape, oldOptions);
534
+ } else {
535
+ strokeOpacity = opacity;
536
+ }
537
+ }
538
+ });
539
+
540
+ document.getElementById("strokeOpacity")?.addEventListener("mouseup", (event) => {
541
+ isdraggingOpacity = false;
542
+ });
543
+
544
+ function cloneOptions(options) {
545
+ return JSON.parse(JSON.stringify(options));
546
+ }
547
+
548
+ function cloneStrokeData(stroke) {
549
+ return {
550
+ points: JSON.parse(JSON.stringify(stroke.points)),
551
+ rotation: stroke.rotation,
552
+ options: cloneOptions(stroke.options)
553
+ };
554
+ }
555
+
556
+ // Event listeners
557
+ // svg.addEventListener('mousedown', handleMouseDown);
558
+ // svg.addEventListener('mousemove', handleMouseMove);
559
+ // svg.addEventListener('mouseup', handleMouseUp);
560
+
561
+ // Bridge freehand tool settings to React sidebar
562
+ window.freehandToolSettings = {
563
+ get strokeColor() { return strokeColor; },
564
+ set strokeColor(v) { strokeColor = v; },
565
+ get strokeWidth() { return strokeThickness; },
566
+ set strokeWidth(v) { strokeThickness = v; },
567
+ get strokeStyle() { return strokeStyleValue; },
568
+ set strokeStyle(v) { strokeStyleValue = v; },
569
+ get thinning() { return strokeThinning; },
570
+ set thinning(v) { strokeThinning = v; },
571
+ get roughness() { return strokeRoughnessValue; },
572
+ set roughness(v) { strokeRoughnessValue = v; },
573
+ get opacity() { return strokeOpacity; },
574
+ set opacity(v) { strokeOpacity = v; },
575
+ };
576
+ window.updateSelectedFreehandStyle = function(changes) {
577
+ if (currentShape && currentShape.shapeName === 'freehandStroke' && currentShape.isSelected) {
578
+ if (changes.stroke !== undefined) { strokeColor = changes.stroke; currentShape.options.stroke = changes.stroke; }
579
+ if (changes.strokeWidth !== undefined) { strokeThickness = changes.strokeWidth; currentShape.options.strokeWidth = changes.strokeWidth; }
580
+ if (changes.thinning !== undefined) { strokeThinning = changes.thinning; currentShape.options.thinning = changes.thinning; }
581
+ if (changes.roughness !== undefined) { strokeRoughnessValue = changes.roughness; currentShape.options.roughness = changes.roughness; }
582
+ if (changes.opacity !== undefined) { strokeOpacity = changes.opacity; currentShape.options.strokeOpacity = changes.opacity; }
583
+ currentShape.draw();
584
+ } else {
585
+ if (changes.stroke !== undefined) strokeColor = changes.stroke;
586
+ if (changes.strokeWidth !== undefined) strokeThickness = changes.strokeWidth;
587
+ if (changes.thinning !== undefined) strokeThinning = changes.thinning;
588
+ if (changes.roughness !== undefined) strokeRoughnessValue = changes.roughness;
589
+ if (changes.opacity !== undefined) strokeOpacity = changes.opacity;
590
+ }
591
+ };
592
+
593
+ // Safety net: if mouseup fires outside the SVG canvas (e.g. on toolbar/overlay),
594
+ // ensure we stop drawing so the stroke doesn't continue when pointer re-enters.
595
+ window.addEventListener('mouseup', () => {
596
+ if (isDrawingStroke) {
597
+ isDrawingStroke = false;
598
+ lastPoint = null;
599
+ if (currentStroke && currentStroke.points && currentStroke.points.length >= 2) {
600
+ currentStroke.draw();
601
+ }
602
+ currentStroke = null;
603
+ }
604
+ });
605
+
606
+ // Also listen for visibility change (e.g. alt-tab while drawing)
607
+ document.addEventListener('visibilitychange', () => {
608
+ if (document.hidden && isDrawingStroke) {
609
+ isDrawingStroke = false;
610
+ lastPoint = null;
611
+ currentStroke = null;
612
+ }
613
+ });
614
+
615
+ export
616
+ {
617
+ handleMouseDown as handleFreehandMouseDown,
618
+ handleMouseMove as handleFreehandMouseMove,
619
+ handleMouseUp as handleFreehandMouseUp,
620
+ }