@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,131 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Eraser tool - extracted from eraserTool.js
|
|
3
|
+
// Depends on globals: svg, isEraserToolActive, ACTION_DELETE, historyStack, redoStack, clearAllSelections
|
|
4
|
+
import { createEraserTrail, updateEraserTrail, fadeOutEraserTrail, forceCleanupEraserTrail, getIsErasing, setIsErasing, getTargetedElements } from '../core/EraserTrail.js';
|
|
5
|
+
|
|
6
|
+
const eraserCursorSVG = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="#222" stroke="white" stroke-width="2"/></svg>')}`;
|
|
7
|
+
|
|
8
|
+
// Expose force cleanup on window so SketchEngine can call it on tool switch
|
|
9
|
+
window.forceCleanupEraserTrail = forceCleanupEraserTrail;
|
|
10
|
+
|
|
11
|
+
// Walk up from any element to find its top-level SVG child (direct child of svg)
|
|
12
|
+
function findTopLevelGroup(element) {
|
|
13
|
+
if (!element || element === svg) return null;
|
|
14
|
+
let current = element;
|
|
15
|
+
while (current && current.parentNode !== svg) {
|
|
16
|
+
current = current.parentNode;
|
|
17
|
+
if (!current || current === document.body) return null;
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- Function to highlight elements under the eraser ---
|
|
23
|
+
function handleElementHighlight(clientX, clientY) {
|
|
24
|
+
if (!getIsErasing()) return;
|
|
25
|
+
const targetedElements = getTargetedElements();
|
|
26
|
+
|
|
27
|
+
const element = document.elementFromPoint(clientX, clientY);
|
|
28
|
+
if (!element || element === svg) return;
|
|
29
|
+
|
|
30
|
+
// Find the top-level SVG child group for this element
|
|
31
|
+
let elementToHighlight = findTopLevelGroup(element);
|
|
32
|
+
|
|
33
|
+
// Skip the eraser trail paths and any pointer-events:none elements
|
|
34
|
+
if (!elementToHighlight) return;
|
|
35
|
+
if (elementToHighlight.style.pointerEvents === 'none') return;
|
|
36
|
+
|
|
37
|
+
if (!targetedElements.has(elementToHighlight)) {
|
|
38
|
+
targetedElements.add(elementToHighlight);
|
|
39
|
+
elementToHighlight.setAttribute("data-original-opacity", elementToHighlight.style.opacity || "1");
|
|
40
|
+
elementToHighlight.dataset.storedOpacity = elementToHighlight.getAttribute("data-original-opacity");
|
|
41
|
+
elementToHighlight.style.opacity = "0.3";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function removeHighlight() {
|
|
46
|
+
const targetedElements = getTargetedElements();
|
|
47
|
+
targetedElements.forEach(element => {
|
|
48
|
+
element.style.opacity = element.getAttribute("data-original-opacity") || "1";
|
|
49
|
+
});
|
|
50
|
+
targetedElements.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removeTargetedElements() {
|
|
54
|
+
const targetedElements = getTargetedElements();
|
|
55
|
+
const elementsToRemove = Array.from(targetedElements);
|
|
56
|
+
|
|
57
|
+
if (elementsToRemove.length > 0) {
|
|
58
|
+
const deletionActions = [];
|
|
59
|
+
|
|
60
|
+
elementsToRemove.forEach(element => {
|
|
61
|
+
const originalOpacity = element.dataset.storedOpacity || "1";
|
|
62
|
+
deletionActions.push({
|
|
63
|
+
type: ACTION_DELETE,
|
|
64
|
+
element: element,
|
|
65
|
+
parent: element.parentNode,
|
|
66
|
+
nextSibling: element.nextSibling,
|
|
67
|
+
originalOpacity: originalOpacity,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Remove from DOM
|
|
71
|
+
if (element.parentNode) {
|
|
72
|
+
element.parentNode.removeChild(element);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Also remove from the global shapes array
|
|
76
|
+
if (window.shapes) {
|
|
77
|
+
const idx = window.shapes.findIndex(s =>
|
|
78
|
+
s.element === element ||
|
|
79
|
+
s.group === element ||
|
|
80
|
+
s.wrapper === element
|
|
81
|
+
);
|
|
82
|
+
if (idx !== -1) {
|
|
83
|
+
window.shapes.splice(idx, 1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (deletionActions.length > 0 && window.historyStack) {
|
|
89
|
+
window.historyStack.push(...deletionActions);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
window.redoStack = [];
|
|
93
|
+
if (typeof clearAllSelections === 'function') clearAllSelections();
|
|
94
|
+
if (typeof updateUndoRedoButtons === 'function') updateUndoRedoButtons();
|
|
95
|
+
targetedElements.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Event Listeners ---
|
|
100
|
+
svg.addEventListener("pointerdown", (e) => {
|
|
101
|
+
if (isEraserToolActive) {
|
|
102
|
+
setIsErasing(true);
|
|
103
|
+
createEraserTrail(e.clientX, e.clientY);
|
|
104
|
+
handleElementHighlight(e.clientX, e.clientY);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
svg.addEventListener("pointermove", (e) => {
|
|
109
|
+
if (isEraserToolActive) {
|
|
110
|
+
svg.style.cursor = `url(${eraserCursorSVG}) 10 10, auto`;
|
|
111
|
+
}
|
|
112
|
+
if (!getIsErasing()) return;
|
|
113
|
+
updateEraserTrail(e.clientX, e.clientY);
|
|
114
|
+
handleElementHighlight(e.clientX, e.clientY);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
svg.addEventListener("pointerup", () => {
|
|
118
|
+
if (!getIsErasing()) return;
|
|
119
|
+
setIsErasing(false);
|
|
120
|
+
removeTargetedElements();
|
|
121
|
+
fadeOutEraserTrail();
|
|
122
|
+
svg.style.cursor = "default";
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
svg.addEventListener("pointerleave", (e) => {
|
|
126
|
+
if (!getIsErasing()) return;
|
|
127
|
+
setIsErasing(false);
|
|
128
|
+
removeTargetedElements();
|
|
129
|
+
fadeOutEraserTrail();
|
|
130
|
+
svg.style.cursor = "default";
|
|
131
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Frame tool event handlers - extracted from frameHolder.js
|
|
3
|
+
import { pushCreateAction, pushDeleteAction, pushTransformAction, pushFrameAttachmentAction } from '../core/UndoRedo.js';
|
|
4
|
+
import { updateAttachedArrows, cleanupAttachments } from './arrowTool.js';
|
|
5
|
+
|
|
6
|
+
let currentFrame = null;
|
|
7
|
+
let isResizing = false;
|
|
8
|
+
let isDragging = false;
|
|
9
|
+
let activeAnchor = null;
|
|
10
|
+
let isDrawingFrame = false;
|
|
11
|
+
let frameStrokeColor = "#888";
|
|
12
|
+
let frameStrokeThickness = 2;
|
|
13
|
+
let frameFillColor = "transparent";
|
|
14
|
+
let frameOpacity = 1;
|
|
15
|
+
let startX, startY;
|
|
16
|
+
let dragOldPosFrame = null;
|
|
17
|
+
|
|
18
|
+
function getSVGCoordsFromMouse(e) {
|
|
19
|
+
const viewBox = svg.viewBox.baseVal;
|
|
20
|
+
const rect = svg.getBoundingClientRect();
|
|
21
|
+
const mouseX = e.clientX - rect.left;
|
|
22
|
+
const mouseY = e.clientY - rect.top;
|
|
23
|
+
const svgX = viewBox.x + (mouseX / rect.width) * viewBox.width;
|
|
24
|
+
const svgY = viewBox.y + (mouseY / rect.height) * viewBox.height;
|
|
25
|
+
return { x: svgX, y: svgY };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// Event handlers for frame tool
|
|
30
|
+
const handleMouseDown = (e) => {
|
|
31
|
+
if (!isFrameToolActive && !isSelectionToolActive) return;
|
|
32
|
+
|
|
33
|
+
const { x, y } = getSVGCoordsFromMouse(e);
|
|
34
|
+
|
|
35
|
+
if (isFrameToolActive) {
|
|
36
|
+
isDrawingFrame = true;
|
|
37
|
+
startX = x;
|
|
38
|
+
startY = y;
|
|
39
|
+
currentFrame = new Frame(x, y, 0, 0, {
|
|
40
|
+
stroke: frameStrokeColor,
|
|
41
|
+
strokeWidth: frameStrokeThickness,
|
|
42
|
+
fill: frameFillColor,
|
|
43
|
+
opacity: frameOpacity
|
|
44
|
+
});
|
|
45
|
+
shapes.push(currentFrame);
|
|
46
|
+
currentShape = currentFrame;
|
|
47
|
+
} else if (isSelectionToolActive) {
|
|
48
|
+
// Handle frame selection — but first check if a contained shape was clicked
|
|
49
|
+
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
50
|
+
if (shapes[i].shapeName === 'frame' && shapes[i].contains(x, y)) {
|
|
51
|
+
const clickedFrame = shapes[i];
|
|
52
|
+
|
|
53
|
+
// Check if click is on a shape inside this frame — let shape handlers deal with it
|
|
54
|
+
let clickedContainedShape = false;
|
|
55
|
+
if (clickedFrame.containedShapes && clickedFrame.containedShapes.length > 0) {
|
|
56
|
+
for (let j = clickedFrame.containedShapes.length - 1; j >= 0; j--) {
|
|
57
|
+
const inner = clickedFrame.containedShapes[j];
|
|
58
|
+
if (inner.contains && inner.contains(x, y)) {
|
|
59
|
+
clickedContainedShape = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// If a contained shape was clicked, don't select the frame — let the
|
|
65
|
+
// EventDispatcher fall through to shape-specific handlers
|
|
66
|
+
if (clickedContainedShape) return;
|
|
67
|
+
|
|
68
|
+
if (currentShape && currentShape !== clickedFrame) {
|
|
69
|
+
currentShape.removeSelection();
|
|
70
|
+
}
|
|
71
|
+
currentShape = clickedFrame;
|
|
72
|
+
currentShape.selectFrame();
|
|
73
|
+
|
|
74
|
+
// Check for anchor interaction
|
|
75
|
+
const anchorIndex = typeof currentShape.isNearAnchor === 'function' ? currentShape.isNearAnchor(x, y) : null;
|
|
76
|
+
if (anchorIndex !== null) {
|
|
77
|
+
activeAnchor = anchorIndex;
|
|
78
|
+
isResizing = true;
|
|
79
|
+
} else {
|
|
80
|
+
isDragging = true;
|
|
81
|
+
startX = x;
|
|
82
|
+
startY = y;
|
|
83
|
+
dragOldPosFrame = {
|
|
84
|
+
x: currentShape.x,
|
|
85
|
+
y: currentShape.y,
|
|
86
|
+
width: currentShape.width,
|
|
87
|
+
height: currentShape.height,
|
|
88
|
+
rotation: currentShape.rotation
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// If no frame was clicked, deselect current
|
|
96
|
+
if (currentShape && currentShape.shapeName === 'frame') {
|
|
97
|
+
currentShape.removeSelection();
|
|
98
|
+
currentShape = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleMouseMove = (e) => {
|
|
104
|
+
const { x, y } = getSVGCoordsFromMouse(e);
|
|
105
|
+
|
|
106
|
+
if (isDrawingFrame && currentFrame) {
|
|
107
|
+
const width = Math.abs(x - startX);
|
|
108
|
+
const height = Math.abs(y - startY);
|
|
109
|
+
currentFrame.x = Math.min(startX, x);
|
|
110
|
+
currentFrame.y = Math.min(startY, y);
|
|
111
|
+
currentFrame.width = width;
|
|
112
|
+
currentFrame.height = height;
|
|
113
|
+
currentFrame.draw();
|
|
114
|
+
} else if (isDragging && currentShape && currentShape.shapeName === 'frame') {
|
|
115
|
+
const dx = x - startX;
|
|
116
|
+
const dy = y - startY;
|
|
117
|
+
|
|
118
|
+
// Use the frame's move method which properly handles contained shapes
|
|
119
|
+
currentShape.move(dx, dy);
|
|
120
|
+
|
|
121
|
+
startX = x;
|
|
122
|
+
startY = y;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleMouseUp = (e) => {
|
|
127
|
+
if (isDrawingFrame && currentFrame) {
|
|
128
|
+
// Check if frame is too small
|
|
129
|
+
if (currentFrame.width < 10 || currentFrame.height < 10) {
|
|
130
|
+
currentFrame.destroy();
|
|
131
|
+
currentFrame = null;
|
|
132
|
+
currentShape = null;
|
|
133
|
+
} else {
|
|
134
|
+
pushCreateAction(currentFrame);
|
|
135
|
+
// Check for shapes that should be contained in the new frame
|
|
136
|
+
currentFrame.updateContainedShapes(true); // Apply clipping immediately for new frames
|
|
137
|
+
|
|
138
|
+
// Auto-select the new frame and switch to selection tool
|
|
139
|
+
const placedFrame = currentFrame;
|
|
140
|
+
if (window.__sketchStoreApi) {
|
|
141
|
+
window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
|
|
142
|
+
} else {
|
|
143
|
+
window.isSelectionToolActive = true;
|
|
144
|
+
}
|
|
145
|
+
currentShape = placedFrame;
|
|
146
|
+
placedFrame.selectFrame();
|
|
147
|
+
}
|
|
148
|
+
isDrawingFrame = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isDragging && dragOldPosFrame && currentShape) {
|
|
152
|
+
const newPos = {
|
|
153
|
+
x: currentShape.x,
|
|
154
|
+
y: currentShape.y,
|
|
155
|
+
width: currentShape.width,
|
|
156
|
+
height: currentShape.height,
|
|
157
|
+
rotation: currentShape.rotation
|
|
158
|
+
};
|
|
159
|
+
pushTransformAction(currentShape, dragOldPosFrame, newPos);
|
|
160
|
+
dragOldPosFrame = null;
|
|
161
|
+
|
|
162
|
+
// Update frame containment after moving frame (but don't re-move contained shapes)
|
|
163
|
+
if (currentShape.shapeName === 'frame') {
|
|
164
|
+
currentShape.updateClipPath();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Only check frame containment for shapes that aren't already in frames
|
|
169
|
+
if (!isDragging) {
|
|
170
|
+
shapes.forEach(shape => {
|
|
171
|
+
if (shape.shapeName !== 'frame' && !shape.parentFrame) {
|
|
172
|
+
shapes.forEach(frame => {
|
|
173
|
+
if (frame.shapeName === 'frame') {
|
|
174
|
+
if (frame.isShapeInFrame(shape)) {
|
|
175
|
+
frame.addShapeToFrame(shape);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
isDrawingFrame = false;
|
|
184
|
+
isDragging = false;
|
|
185
|
+
isResizing = false;
|
|
186
|
+
activeAnchor = null;
|
|
187
|
+
svg.style.cursor = 'default';
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
Frame.prototype.isNearAnchor = function(x, y) {
|
|
191
|
+
const anchorSize = 10 / currentZoom;
|
|
192
|
+
|
|
193
|
+
// Define anchor positions
|
|
194
|
+
const anchorPositions = [
|
|
195
|
+
{ x: this.x, y: this.y },
|
|
196
|
+
{ x: this.x + this.width / 2, y: this.y },
|
|
197
|
+
{ x: this.x + this.width, y: this.y },
|
|
198
|
+
{ x: this.x + this.width, y: this.y + this.height / 2 },
|
|
199
|
+
{ x: this.x + this.width, y: this.y + this.height },
|
|
200
|
+
{ x: this.x + this.width / 2, y: this.y + this.height },
|
|
201
|
+
{ x: this.x, y: this.y + this.height },
|
|
202
|
+
{ x: this.x, y: this.y + this.height / 2 },
|
|
203
|
+
{ x: this.x + this.width / 2, y: this.y - 30 / currentZoom } // Rotation handle
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < anchorPositions.length; i++) {
|
|
207
|
+
const pos = anchorPositions[i];
|
|
208
|
+
const distance = Math.sqrt((x - pos.x) ** 2 + (y - pos.y) ** 2);
|
|
209
|
+
if (distance <= anchorSize) {
|
|
210
|
+
return i;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Delete functionality
|
|
217
|
+
function deleteCurrentFrame() {
|
|
218
|
+
if (currentShape && currentShape.shapeName === 'frame') {
|
|
219
|
+
const idx = shapes.indexOf(currentShape);
|
|
220
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
221
|
+
if (currentShape.group.parentNode) {
|
|
222
|
+
currentShape.group.parentNode.removeChild(currentShape.group);
|
|
223
|
+
}
|
|
224
|
+
pushDeleteAction(currentShape);
|
|
225
|
+
currentShape = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
document.addEventListener('keydown', (e) => {
|
|
230
|
+
if (e.key === 'Delete' && currentShape && currentShape.shapeName === 'frame') {
|
|
231
|
+
deleteCurrentFrame();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
window.Frame = Frame;
|
|
236
|
+
|
|
237
|
+
export {
|
|
238
|
+
handleMouseDown as handleMouseDownFrame,
|
|
239
|
+
handleMouseMove as handleMouseMoveFrame,
|
|
240
|
+
handleMouseUp as handleMouseUpFrame
|
|
241
|
+
};
|