@elixpo/lixsketch 5.6.11 → 5.6.12
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/dist/react/Arrow-YIOAH45F.js +9 -0
- package/dist/react/{Circle-5WL5HZA5.js → Circle-BZANVNWQ.js} +3 -3
- package/dist/react/{CopyPaste-V7AY56H3.js → CopyPaste-IY5TF5Z2.js} +6 -6
- package/dist/react/{EraserTrail-PA6IQLQN.js → EraserTrail-F3ZJNWNL.js} +2 -2
- package/dist/react/{Line-YONPLUYT.js → Line-7IGMPC5Q.js} +3 -3
- package/dist/react/{Rectangle-7KMA3SC6.js → Rectangle-G3MPA72T.js} +3 -3
- package/dist/react/{SceneSerializer-7O72LEZP.js → SceneSerializer-X5GJFI2E.js} +6 -6
- package/dist/react/{SketchEngine-VTWURP2K.js → SketchEngine-6UBUCW5L.js} +10 -10
- package/dist/react/{chunk-TCWCC4Z4.js → chunk-3ROBBYUS.js} +30 -12
- package/dist/react/chunk-3ROBBYUS.js.map +7 -0
- package/dist/react/{chunk-JYBJXTLM.js → chunk-6DJ2UFEY.js} +4 -3
- package/dist/react/chunk-6DJ2UFEY.js.map +7 -0
- package/dist/react/chunk-6KNC7RDT.js +59 -0
- package/dist/react/chunk-6KNC7RDT.js.map +7 -0
- package/dist/react/{chunk-5OUBTHHO.js → chunk-PK7MU63Q.js} +33 -12
- package/dist/react/chunk-PK7MU63Q.js.map +7 -0
- package/dist/react/{chunk-WCA3FGWE.js → chunk-YXJMIL6G.js} +4 -3
- package/dist/react/chunk-YXJMIL6G.js.map +7 -0
- package/dist/react/{chunk-VJDNKJBX.js → chunk-ZTSW4DOB.js} +4 -3
- package/dist/react/chunk-ZTSW4DOB.js.map +7 -0
- package/dist/react/{eraserTool-2GS2LQCS.js → eraserTool-P6WR35AE.js} +11 -5
- package/dist/react/eraserTool-P6WR35AE.js.map +7 -0
- package/dist/react/index.js +41 -8
- package/dist/react/index.js.map +3 -3
- package/dist/react/{laserTool-XVMV63AV.js → laserTool-L3MNCPTS.js} +15 -8
- package/dist/react/laserTool-L3MNCPTS.js.map +7 -0
- package/package.json +1 -1
- package/src/core/EraserTrail.js +36 -11
- package/src/core/ShapeLabelLayout.js +42 -0
- package/src/react/components/sidebars/ShapeSidebar.jsx +28 -4
- package/src/react/store/useSketchStore.js +5 -1
- package/src/shapes/Arrow.js +2 -1
- package/src/shapes/Circle.js +2 -1
- package/src/shapes/Line.js +2 -1
- package/src/shapes/Rectangle.js +32 -10
- package/src/tools/eraserTool.js +9 -3
- package/src/tools/laserTool.js +14 -8
- package/dist/react/Arrow-QMR77LGQ.js +0 -9
- package/dist/react/chunk-5OUBTHHO.js.map +0 -7
- package/dist/react/chunk-JYBJXTLM.js.map +0 -7
- package/dist/react/chunk-M7OYYJC6.js +0 -17
- package/dist/react/chunk-M7OYYJC6.js.map +0 -7
- package/dist/react/chunk-TCWCC4Z4.js.map +0 -7
- package/dist/react/chunk-VJDNKJBX.js.map +0 -7
- package/dist/react/chunk-WCA3FGWE.js.map +0 -7
- package/dist/react/eraserTool-2GS2LQCS.js.map +0 -7
- package/dist/react/laserTool-XVMV63AV.js.map +0 -7
- /package/dist/react/{Arrow-QMR77LGQ.js.map → Arrow-YIOAH45F.js.map} +0 -0
- /package/dist/react/{Circle-5WL5HZA5.js.map → Circle-BZANVNWQ.js.map} +0 -0
- /package/dist/react/{CopyPaste-V7AY56H3.js.map → CopyPaste-IY5TF5Z2.js.map} +0 -0
- /package/dist/react/{EraserTrail-PA6IQLQN.js.map → EraserTrail-F3ZJNWNL.js.map} +0 -0
- /package/dist/react/{Line-YONPLUYT.js.map → Line-7IGMPC5Q.js.map} +0 -0
- /package/dist/react/{Rectangle-7KMA3SC6.js.map → Rectangle-G3MPA72T.js.map} +0 -0
- /package/dist/react/{SceneSerializer-7O72LEZP.js.map → SceneSerializer-X5GJFI2E.js.map} +0 -0
- /package/dist/react/{SketchEngine-VTWURP2K.js.map → SketchEngine-6UBUCW5L.js.map} +0 -0
|
@@ -9,10 +9,11 @@ var fadeOutDuration = 1200;
|
|
|
9
9
|
var baseLaserOpacity = 0.85;
|
|
10
10
|
var baseLaserWidth = 3.5;
|
|
11
11
|
var glowWidth = 10;
|
|
12
|
-
var minDistanceThreshold =
|
|
13
|
-
var smoothingWindow =
|
|
12
|
+
var minDistanceThreshold = 0.75;
|
|
13
|
+
var smoothingWindow = 5;
|
|
14
14
|
var drawingAnimationId = null;
|
|
15
15
|
var fadingAnimationId = null;
|
|
16
|
+
var activePointerId = null;
|
|
16
17
|
function screenToViewBoxPoint(x, y) {
|
|
17
18
|
const CTM = svg.getScreenCTM();
|
|
18
19
|
if (!CTM) return null;
|
|
@@ -187,7 +188,7 @@ function fadeLaserTrail(laser) {
|
|
|
187
188
|
fadingAnimationId = requestAnimationFrame(fadingLasersLoop);
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
|
-
svg.addEventListener("
|
|
191
|
+
svg.addEventListener("pointerdown", (e) => {
|
|
191
192
|
if (!isLaserToolActive) return;
|
|
192
193
|
if (e.target !== svg) {
|
|
193
194
|
const isUIElement = e.target.closest(".selection-box, .resize-handle");
|
|
@@ -200,6 +201,8 @@ svg.addEventListener("mousedown", (e) => {
|
|
|
200
201
|
fadeLaserTrail(lasers[lasers.length - 1]);
|
|
201
202
|
}
|
|
202
203
|
isDrawing = true;
|
|
204
|
+
activePointerId = e.pointerId;
|
|
205
|
+
svg.setPointerCapture?.(e.pointerId);
|
|
203
206
|
const laserId = "laserGroup_" + Date.now() + "_" + Math.random().toString(16).slice(2);
|
|
204
207
|
const newLaser = {
|
|
205
208
|
id: laserId,
|
|
@@ -210,16 +213,19 @@ svg.addEventListener("mousedown", (e) => {
|
|
|
210
213
|
drawingAnimationId = requestAnimationFrame(drawingLoop);
|
|
211
214
|
}
|
|
212
215
|
});
|
|
213
|
-
svg.addEventListener("
|
|
216
|
+
svg.addEventListener("pointermove", (e) => {
|
|
214
217
|
if (isLaserToolActive) {
|
|
215
218
|
svg.style.cursor = `url(${lazerCursor}) 0 0, auto`;
|
|
216
219
|
}
|
|
217
220
|
if (!isDrawing) return;
|
|
218
|
-
|
|
221
|
+
const events = typeof e.getCoalescedEvents === "function" ? e.getCoalescedEvents() : [e];
|
|
222
|
+
events.forEach((point) => addPointToCurrentLaser(point.clientX, point.clientY));
|
|
219
223
|
});
|
|
220
|
-
svg.addEventListener("
|
|
224
|
+
svg.addEventListener("pointerup", (e) => {
|
|
221
225
|
if (!isDrawing) return;
|
|
222
226
|
isDrawing = false;
|
|
227
|
+
if (activePointerId !== null) svg.releasePointerCapture?.(activePointerId);
|
|
228
|
+
activePointerId = null;
|
|
223
229
|
if (lasers.length > 0) {
|
|
224
230
|
fadeLaserTrail(lasers[lasers.length - 1]);
|
|
225
231
|
}
|
|
@@ -229,11 +235,12 @@ svg.addEventListener("mouseup", (e) => {
|
|
|
229
235
|
svg.style.cursor = "default";
|
|
230
236
|
}
|
|
231
237
|
});
|
|
232
|
-
svg.addEventListener("
|
|
238
|
+
svg.addEventListener("pointercancel", () => {
|
|
233
239
|
if (!isDrawing) return;
|
|
234
240
|
isDrawing = false;
|
|
241
|
+
activePointerId = null;
|
|
235
242
|
if (lasers.length > 0) {
|
|
236
243
|
fadeLaserTrail(lasers[lasers.length - 1]);
|
|
237
244
|
}
|
|
238
245
|
});
|
|
239
|
-
//# sourceMappingURL=laserTool-
|
|
246
|
+
//# sourceMappingURL=laserTool-L3MNCPTS.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/tools/laserTool.js"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable */\n// Laser tool - smooth laser pointer with fade trail\n// Depends on globals: svg, currentZoom, isLaserToolActive\n\nconst lazerCursor = `data:image/svg+xml;base64,${btoa('<svg viewBox=\"0 0 24 24\" stroke-width=\"1\" width=\"28\" height=\"28\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6.164 11.755a5.314 5.314 0 0 1-4.932-5.298 5.314 5.314 0 0 1 5.311-5.311 5.314 5.314 0 0 1 5.307 5.113l8.773 8.773a3.322 3.322 0 0 1 0 4.696l-.895.895a3.322 3.322 0 0 1-4.696 0l-8.868-8.868Z\" style=\"fill:#fff\"/><path stroke=\"#1b1b1f\" fill=\"#fff\" d=\"m7.868 11.113 7.773 7.774a2.359 2.359 0 0 0 1.667.691 2.368 2.368 0 0 0 2.357-2.358c0-.625-.248-1.225-.69-1.667L11.201 7.78 9.558 9.469l-1.69 1.643v.001Zm10.273 3.606-3.333 3.333m-3.25-6.583 2 2m-7-7 3 3M3.664 3.625l1 1M2.529 6.922l1.407-.144m5.735-2.932-1.118.866M4.285 9.823l.758-1.194m1.863-6.207-.13 1.408\"/></svg>')}`;\n\nlet isDrawing = false;\nlet lasers = [];\nlet fadingLasers = [];\n\nconst fadeOutDuration = 1200;\nconst baseLaserOpacity = 0.85;\nconst baseLaserWidth = 3.5;\nconst glowWidth = 10;\nconst minDistanceThreshold = 0.75;\nconst smoothingWindow = 5; // points to average for smoothing\n\nlet drawingAnimationId = null;\nlet fadingAnimationId = null;\nlet activePointerId = null;\n\nfunction screenToViewBoxPoint(x, y) {\n const CTM = svg.getScreenCTM();\n if (!CTM) return null;\n try {\n const inverseCTM = CTM.inverse();\n return {\n x: (x - CTM.e) * inverseCTM.a + (y - CTM.f) * inverseCTM.c,\n y: (x - CTM.e) * inverseCTM.b + (y - CTM.f) * inverseCTM.d\n };\n } catch (e) {\n return {\n x: (x - CTM.e) / CTM.a,\n y: (y - CTM.f) / CTM.d\n };\n }\n}\n\nfunction distance(p1, p2) {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.sqrt(dx * dx + dy * dy);\n}\n\n// Smooth points using a moving average to eliminate micro-jitter\nfunction smoothPoints(points) {\n if (points.length <= 2) return points;\n const result = [points[0]]; // keep first point\n const half = Math.floor(smoothingWindow / 2);\n for (let i = 1; i < points.length - 1; i++) {\n let sx = 0, sy = 0, count = 0;\n for (let j = Math.max(0, i - half); j <= Math.min(points.length - 1, i + half); j++) {\n sx += points[j].x;\n sy += points[j].y;\n count++;\n }\n result.push({ x: sx / count, y: sy / count, timestamp: points[i].timestamp });\n }\n result.push(points[points.length - 1]); // keep last point\n return result;\n}\n\n// Build a smooth SVG path using Catmull-Rom \u2192 cubic bezier conversion\nfunction buildSmoothPath(rawPoints) {\n if (rawPoints.length === 0) return '';\n if (rawPoints.length === 1) return `M ${rawPoints[0].x} ${rawPoints[0].y}`;\n\n const points = smoothPoints(rawPoints);\n\n let d = `M ${points[0].x} ${points[0].y}`;\n\n if (points.length === 2) {\n d += ` L ${points[1].x} ${points[1].y}`;\n return d;\n }\n\n // Catmull-Rom to cubic bezier with tension=0.5 for rounder curves\n const tension = 0.5;\n for (let i = 0; i < points.length - 1; i++) {\n const p0 = points[i === 0 ? 0 : i - 1];\n const p1 = points[i];\n const p2 = points[i + 1];\n const p3 = points[i + 2] || p2;\n\n const cp1x = p1.x + (p2.x - p0.x) * tension / 3;\n const cp1y = p1.y + (p2.y - p0.y) * tension / 3;\n const cp2x = p2.x - (p3.x - p1.x) * tension / 3;\n const cp2y = p2.y - (p3.y - p1.y) * tension / 3;\n\n d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`;\n }\n return d;\n}\n\n// Create SVG group with glow + core path elements\nfunction createLaserGroup(id) {\n const group = document.createElementNS(\"http://www.w3.org/2000/svg\", \"g\");\n group.setAttribute(\"id\", id);\n group.style.pointerEvents = \"none\";\n\n // Outer glow\n const glowPath = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\");\n glowPath.setAttribute(\"fill\", \"none\");\n glowPath.setAttribute(\"stroke-linecap\", \"round\");\n glowPath.setAttribute(\"stroke-linejoin\", \"round\");\n glowPath.classList.add(\"laser-glow\");\n group.appendChild(glowPath);\n\n // Core line\n const corePath = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\");\n corePath.setAttribute(\"fill\", \"none\");\n corePath.setAttribute(\"stroke-linecap\", \"round\");\n corePath.setAttribute(\"stroke-linejoin\", \"round\");\n corePath.classList.add(\"laser-core\");\n group.appendChild(corePath);\n\n svg.insertBefore(group, svg.firstChild);\n return group;\n}\n\nfunction updateLaserAppearance(laser, isFinalFade = false) {\n let laserGroup = document.getElementById(laser.id);\n\n const currentTime = performance.now();\n let points = isFinalFade ? laser.initialPoints : [...laser.points];\n\n if (!isFinalFade) {\n points = points.filter(p => (currentTime - p.timestamp) < fadeOutDuration);\n laser.points = points;\n }\n\n if (points.length === 0) {\n if (laserGroup) laserGroup.remove();\n if (isFinalFade) fadingLasers = fadingLasers.filter(l => l.id !== laser.id);\n else lasers = lasers.filter(l => l.id !== laser.id);\n return;\n }\n\n if (!laserGroup) {\n laserGroup = createLaserGroup(laser.id);\n }\n\n const glowPath = laserGroup.querySelector('.laser-glow');\n const corePath = laserGroup.querySelector('.laser-core');\n\n // Compute overall opacity from age + fade\n const oldestAge = currentTime - points[0].timestamp;\n const newestAge = currentTime - points[points.length - 1].timestamp;\n const avgAge = (oldestAge + newestAge) / 2;\n const ageProgress = Math.min(1, avgAge / fadeOutDuration);\n const easedFade = 1 - ageProgress * ageProgress; // quadratic ease\n\n let opacity = baseLaserOpacity * easedFade;\n let coreW = baseLaserWidth / currentZoom;\n let glowW = glowWidth / currentZoom;\n\n if (isFinalFade) {\n opacity *= laser.fadeProgress;\n coreW *= laser.fadeProgress;\n glowW *= laser.fadeProgress;\n }\n\n opacity = Math.max(0.02, opacity);\n coreW = Math.max(0.5 / currentZoom, coreW);\n glowW = Math.max(1 / currentZoom, glowW);\n\n const pathD = buildSmoothPath(points);\n\n // Update glow\n glowPath.setAttribute(\"d\", pathD);\n glowPath.setAttribute(\"stroke\", `rgba(255, 50, 50, ${opacity * 0.25})`);\n glowPath.setAttribute(\"stroke-width\", glowW);\n\n // Update core\n corePath.setAttribute(\"d\", pathD);\n corePath.setAttribute(\"stroke\", `rgba(255, 50, 50, ${opacity})`);\n corePath.setAttribute(\"stroke-width\", coreW);\n}\n\n\n// Add a point directly from mousemove (high frequency capture)\nfunction addPointToCurrentLaser(screenX, screenY) {\n const currentLaser = lasers[lasers.length - 1];\n if (!currentLaser) return;\n\n const svgPoint = screenToViewBoxPoint(screenX, screenY);\n if (!svgPoint) return;\n\n const lastPt = currentLaser.points[currentLaser.points.length - 1];\n const adjustedThreshold = minDistanceThreshold / currentZoom;\n if (!lastPt || distance(svgPoint, lastPt) >= adjustedThreshold) {\n currentLaser.points.push({ x: svgPoint.x, y: svgPoint.y, timestamp: performance.now() });\n }\n}\n\nfunction drawingLoop() {\n if (!isDrawing) {\n drawingAnimationId = null;\n return;\n }\n\n const currentLaser = lasers[lasers.length - 1];\n if (currentLaser) {\n updateLaserAppearance(currentLaser, false);\n }\n drawingAnimationId = requestAnimationFrame(drawingLoop);\n}\n\n\nfunction fadingLasersLoop() {\n if (fadingLasers.length === 0) {\n fadingAnimationId = null;\n return;\n }\n\n const currentTime = performance.now();\n\n for (let i = fadingLasers.length - 1; i >= 0; i--) {\n const laser = fadingLasers[i];\n const elapsed = currentTime - laser.fadeStartTime;\n const rawProgress = Math.min(1, elapsed / fadeOutDuration);\n laser.fadeProgress = 1 - rawProgress * rawProgress * rawProgress; // cubic ease out\n\n updateLaserAppearance(laser, true);\n\n if (laser.fadeProgress <= 0.01) {\n const laserGroup = document.getElementById(laser.id);\n if (laserGroup) laserGroup.remove();\n fadingLasers.splice(i, 1);\n }\n }\n\n fadingAnimationId = requestAnimationFrame(fadingLasersLoop);\n}\n\n\nfunction fadeLaserTrail(laser) {\n lasers = lasers.filter(l => l.id !== laser.id);\n\n laser.fadeStartTime = performance.now();\n laser.initialPoints = [...laser.points];\n laser.fadeProgress = 1;\n\n fadingLasers.push(laser);\n\n if (!fadingAnimationId) {\n fadingAnimationId = requestAnimationFrame(fadingLasersLoop);\n }\n}\n\n\nsvg.addEventListener(\"pointerdown\", (e) => {\n if (!isLaserToolActive) return;\n\n if (e.target !== svg) {\n const isUIElement = e.target.closest('.selection-box, .resize-handle');\n if (isUIElement) return;\n }\n\n let screenPoint = { x: e.clientX, y: e.clientY };\n let startSvgPoint = screenToViewBoxPoint(screenPoint.x, screenPoint.y);\n if (!startSvgPoint) return;\n\n // If a drawing is already in progress, trigger its fade-out\n if (isDrawing && lasers.length > 0) {\n fadeLaserTrail(lasers[lasers.length - 1]);\n }\n\n isDrawing = true;\n activePointerId = e.pointerId;\n svg.setPointerCapture?.(e.pointerId);\n\n const laserId = \"laserGroup_\" + Date.now() + \"_\" + Math.random().toString(16).slice(2);\n const newLaser = {\n id: laserId,\n points: [{ x: startSvgPoint.x, y: startSvgPoint.y, timestamp: performance.now() }],\n };\n lasers.push(newLaser);\n\n if (!drawingAnimationId) {\n drawingAnimationId = requestAnimationFrame(drawingLoop);\n }\n});\n\nsvg.addEventListener(\"pointermove\", (e) => {\n if (isLaserToolActive) {\n svg.style.cursor = `url(${lazerCursor}) 0 0, auto`;\n }\n\n if (!isDrawing) return;\n\n const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [e];\n events.forEach((point) => addPointToCurrentLaser(point.clientX, point.clientY));\n});\n\nsvg.addEventListener(\"pointerup\", (e) => {\n if (!isDrawing) return;\n\n isDrawing = false;\n if (activePointerId !== null) svg.releasePointerCapture?.(activePointerId);\n activePointerId = null;\n\n if (lasers.length > 0) {\n fadeLaserTrail(lasers[lasers.length - 1]);\n }\n\n if (isLaserToolActive) {\n svg.style.cursor = `url(${lazerCursor}) 0 0, auto`;\n } else {\n svg.style.cursor = 'default';\n }\n});\n\nsvg.addEventListener(\"pointercancel\", () => {\n if (!isDrawing) return;\n\n isDrawing = false;\n activePointerId = null;\n\n if (lasers.length > 0) {\n fadeLaserTrail(lasers[lasers.length - 1]);\n }\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
package/src/core/EraserTrail.js
CHANGED
|
@@ -8,14 +8,22 @@ let eraserGlow = null;
|
|
|
8
8
|
let eraserPoints = [];
|
|
9
9
|
let targetedElements = new Set();
|
|
10
10
|
|
|
11
|
-
const MAX_TRAIL_LENGTH =
|
|
12
|
-
const FADE_DURATION =
|
|
13
|
-
const MIN_POINT_DISTANCE = 1
|
|
14
|
-
const SMOOTHING_WINDOW =
|
|
11
|
+
const MAX_TRAIL_LENGTH = 28;
|
|
12
|
+
const FADE_DURATION = 320;
|
|
13
|
+
const MIN_POINT_DISTANCE = 1;
|
|
14
|
+
const SMOOTHING_WINDOW = 5;
|
|
15
15
|
|
|
16
16
|
let fadeAnimationId = null;
|
|
17
17
|
let fadeStartTime = null;
|
|
18
18
|
let fadeInitialPoints = null;
|
|
19
|
+
let renderAnimationId = null;
|
|
20
|
+
|
|
21
|
+
function trailPalette() {
|
|
22
|
+
const isLight = document.documentElement.dataset.canvasTheme === 'light' || document.body?.classList.contains('theme-light');
|
|
23
|
+
return isLight
|
|
24
|
+
? { core: '49, 40, 63', glow: '124, 89, 195' }
|
|
25
|
+
: { core: '245, 239, 251', glow: '199, 164, 255' };
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
// Getter/setter for isErasing so eraserTool can share state
|
|
21
29
|
function getIsErasing() { return isErasing; }
|
|
@@ -98,6 +106,10 @@ function safeRemove(el) {
|
|
|
98
106
|
|
|
99
107
|
// Remove all trail elements and reset state
|
|
100
108
|
function cleanupTrail() {
|
|
109
|
+
if (renderAnimationId) {
|
|
110
|
+
cancelAnimationFrame(renderAnimationId);
|
|
111
|
+
renderAnimationId = null;
|
|
112
|
+
}
|
|
101
113
|
if (fadeAnimationId) {
|
|
102
114
|
cancelAnimationFrame(fadeAnimationId);
|
|
103
115
|
fadeAnimationId = null;
|
|
@@ -119,7 +131,8 @@ function createEraserTrail(x, y) {
|
|
|
119
131
|
// Outer glow
|
|
120
132
|
eraserGlow = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
121
133
|
eraserGlow.setAttribute("fill", "none");
|
|
122
|
-
|
|
134
|
+
const palette = trailPalette();
|
|
135
|
+
eraserGlow.setAttribute("stroke", `rgba(${palette.glow}, 0.20)`);
|
|
123
136
|
eraserGlow.setAttribute("stroke-width", 14 / currentZoom);
|
|
124
137
|
eraserGlow.setAttribute("stroke-linecap", "round");
|
|
125
138
|
eraserGlow.setAttribute("stroke-linejoin", "round");
|
|
@@ -129,7 +142,7 @@ function createEraserTrail(x, y) {
|
|
|
129
142
|
// Core path
|
|
130
143
|
eraserPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
131
144
|
eraserPath.setAttribute("fill", "none");
|
|
132
|
-
eraserPath.setAttribute("stroke",
|
|
145
|
+
eraserPath.setAttribute("stroke", `rgba(${palette.core}, 0.62)`);
|
|
133
146
|
eraserPath.setAttribute("stroke-width", 5 / currentZoom);
|
|
134
147
|
eraserPath.setAttribute("stroke-linecap", "round");
|
|
135
148
|
eraserPath.setAttribute("stroke-linejoin", "round");
|
|
@@ -158,7 +171,12 @@ function updateEraserTrail(x, y) {
|
|
|
158
171
|
eraserPoints.shift();
|
|
159
172
|
}
|
|
160
173
|
|
|
161
|
-
|
|
174
|
+
if (!renderAnimationId) {
|
|
175
|
+
renderAnimationId = requestAnimationFrame(() => {
|
|
176
|
+
renderAnimationId = null;
|
|
177
|
+
renderTrail(eraserPoints, 1.0);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
// Render the trail with given points and opacity multiplier
|
|
@@ -168,16 +186,17 @@ function renderTrail(points, opacityMul) {
|
|
|
168
186
|
const pathD = buildSmoothPath(points);
|
|
169
187
|
const coreW = Math.max(1.5 / currentZoom, (5 / currentZoom) * opacityMul);
|
|
170
188
|
const glowW = Math.max(3 / currentZoom, (14 / currentZoom) * opacityMul);
|
|
171
|
-
const coreOpacity = 0.
|
|
172
|
-
const glowOpacity = 0.
|
|
189
|
+
const coreOpacity = 0.62 * opacityMul;
|
|
190
|
+
const glowOpacity = 0.20 * opacityMul;
|
|
191
|
+
const palette = trailPalette();
|
|
173
192
|
|
|
174
193
|
eraserPath.setAttribute("d", pathD);
|
|
175
|
-
eraserPath.setAttribute("stroke", `rgba(
|
|
194
|
+
eraserPath.setAttribute("stroke", `rgba(${palette.core}, ${coreOpacity})`);
|
|
176
195
|
eraserPath.setAttribute("stroke-width", coreW);
|
|
177
196
|
|
|
178
197
|
if (eraserGlow) {
|
|
179
198
|
eraserGlow.setAttribute("d", pathD);
|
|
180
|
-
eraserGlow.setAttribute("stroke", `rgba(
|
|
199
|
+
eraserGlow.setAttribute("stroke", `rgba(${palette.glow}, ${glowOpacity})`);
|
|
181
200
|
eraserGlow.setAttribute("stroke-width", glowW);
|
|
182
201
|
}
|
|
183
202
|
}
|
|
@@ -189,6 +208,12 @@ function fadeOutEraserTrail() {
|
|
|
189
208
|
return;
|
|
190
209
|
}
|
|
191
210
|
|
|
211
|
+
if (renderAnimationId) {
|
|
212
|
+
cancelAnimationFrame(renderAnimationId);
|
|
213
|
+
renderAnimationId = null;
|
|
214
|
+
renderTrail(eraserPoints, 1.0);
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
// Cancel any existing fade
|
|
193
218
|
if (fadeAnimationId) {
|
|
194
219
|
cancelAnimationFrame(fadeAnimationId);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const DEFAULT_SHAPE_LABEL_FONT_SIZE = 18;
|
|
2
|
+
|
|
3
|
+
function fallbackWidth(text, fontSize) {
|
|
4
|
+
return [...text].reduce((width, character) => {
|
|
5
|
+
if (/\s/.test(character)) return width + fontSize * 0.34;
|
|
6
|
+
if (/[ilI1.,'`]/.test(character)) return width + fontSize * 0.32;
|
|
7
|
+
if (/[MW@#%]/.test(character)) return width + fontSize * 0.88;
|
|
8
|
+
return width + fontSize * 0.59;
|
|
9
|
+
}, 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function browserTextWidth(text, fontSize) {
|
|
13
|
+
if (typeof document === 'undefined') return null;
|
|
14
|
+
try {
|
|
15
|
+
const canvas = browserTextWidth.canvas || (browserTextWidth.canvas = document.createElement('canvas'));
|
|
16
|
+
const context = canvas.getContext?.('2d');
|
|
17
|
+
if (!context) return null;
|
|
18
|
+
context.font = `${fontSize}px lixFont, sans-serif`;
|
|
19
|
+
return context.measureText(text).width;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function measureShapeLabel(label, fontSize = DEFAULT_SHAPE_LABEL_FONT_SIZE) {
|
|
26
|
+
const lines = String(label ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
27
|
+
const safeLines = lines.length ? lines : [''];
|
|
28
|
+
const widths = safeLines.map((line) => browserTextWidth(line, fontSize) ?? fallbackWidth(line, fontSize));
|
|
29
|
+
const lineHeight = fontSize * 1.3;
|
|
30
|
+
const contentWidth = Math.max(0, ...widths);
|
|
31
|
+
const contentHeight = Math.max(lineHeight, safeLines.length * lineHeight);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
lines: safeLines,
|
|
35
|
+
widths,
|
|
36
|
+
lineHeight,
|
|
37
|
+
contentWidth,
|
|
38
|
+
contentHeight,
|
|
39
|
+
requiredWidth: Math.ceil(contentWidth + 32),
|
|
40
|
+
requiredHeight: Math.ceil(contentHeight + 24),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react'
|
|
4
|
+
import { createPortal } from 'react-dom'
|
|
4
5
|
import useSketchStore from '../../store/useSketchStore'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -10,12 +11,30 @@ import useSketchStore from '../../store/useSketchStore'
|
|
|
10
11
|
export function ToolbarButton({ icon, preview, children, tooltip, label = tooltip }) {
|
|
11
12
|
const [open, setOpen] = useState(false)
|
|
12
13
|
const ref = useRef(null)
|
|
14
|
+
const panelRef = useRef(null)
|
|
15
|
+
const [panelPosition, setPanelPosition] = useState(null)
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!open) return
|
|
19
|
+
const updatePosition = () => {
|
|
20
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
21
|
+
if (!rect) return
|
|
22
|
+
setPanelPosition({ left: rect.left + rect.width / 2, top: rect.top - 12 })
|
|
23
|
+
}
|
|
24
|
+
updatePosition()
|
|
25
|
+
window.addEventListener('resize', updatePosition)
|
|
26
|
+
window.addEventListener('scroll', updatePosition, true)
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('resize', updatePosition)
|
|
29
|
+
window.removeEventListener('scroll', updatePosition, true)
|
|
30
|
+
}
|
|
31
|
+
}, [open])
|
|
13
32
|
|
|
14
33
|
// Close on outside click
|
|
15
34
|
useEffect(() => {
|
|
16
35
|
if (!open) return
|
|
17
36
|
const handler = (e) => {
|
|
18
|
-
if (ref.current && !
|
|
37
|
+
if (!ref.current?.contains(e.target) && !panelRef.current?.contains(e.target)) setOpen(false)
|
|
19
38
|
}
|
|
20
39
|
document.addEventListener('mousedown', handler)
|
|
21
40
|
return () => document.removeEventListener('mousedown', handler)
|
|
@@ -42,14 +61,19 @@ export function ToolbarButton({ icon, preview, children, tooltip, label = toolti
|
|
|
42
61
|
</svg>
|
|
43
62
|
</button>
|
|
44
63
|
|
|
45
|
-
{open && (
|
|
46
|
-
<div
|
|
64
|
+
{open && panelPosition && createPortal(
|
|
65
|
+
<div
|
|
66
|
+
ref={panelRef}
|
|
67
|
+
className="fixed -translate-x-1/2 -translate-y-full z-[10050] max-w-[calc(100vw-16px)]"
|
|
68
|
+
style={panelPosition}
|
|
69
|
+
>
|
|
47
70
|
<div className="bg-surface-card border border-border-light rounded-xl p-3 shadow-xl shadow-black/10 min-w-max">
|
|
48
71
|
{children}
|
|
49
72
|
</div>
|
|
50
73
|
{/* Arrow pointer */}
|
|
51
74
|
<div className="absolute -bottom-[5px] left-1/2 -translate-x-1/2 w-2.5 h-2.5 rotate-45 bg-surface-card border-r border-b border-border-light" />
|
|
52
|
-
</div
|
|
75
|
+
</div>,
|
|
76
|
+
document.body,
|
|
53
77
|
)}
|
|
54
78
|
</div>
|
|
55
79
|
)
|
|
@@ -123,7 +123,11 @@ const useSketchStore = create((set, get) => ({
|
|
|
123
123
|
getCursor: () => {
|
|
124
124
|
const tool = get().activeTool
|
|
125
125
|
if (tool === TOOLS.ERASER) {
|
|
126
|
-
|
|
126
|
+
const isLight = typeof document !== 'undefined' && document.documentElement.dataset.canvasTheme === 'light'
|
|
127
|
+
const fill = isLight ? '#31283f' : '#f5effb'
|
|
128
|
+
const stroke = isLight ? '#ffffff' : '#31283f'
|
|
129
|
+
const svg = `<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="${fill}" stroke="${stroke}" stroke-width="2"/></svg>`
|
|
130
|
+
return `url("data:image/svg+xml;base64,${typeof btoa !== 'undefined' ? btoa(svg) : ''}") 10 10, auto`
|
|
127
131
|
}
|
|
128
132
|
if (tool === TOOLS.LASER) {
|
|
129
133
|
return `url("data:image/svg+xml;base64,${typeof btoa !== 'undefined' ? btoa('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>') : ''}") 0 0, auto`
|
package/src/shapes/Arrow.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
3
3
|
// Arrow shape class - extracted from drawArrow.js
|
|
4
4
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom
|
|
5
|
+
import { DEFAULT_SHAPE_LABEL_FONT_SIZE } from '../core/ShapeLabelLayout.js';
|
|
5
6
|
|
|
6
7
|
let isDragging = false;
|
|
7
8
|
let hoveredFrameArrow = null;
|
|
@@ -56,7 +57,7 @@ class Arrow {
|
|
|
56
57
|
this.label = options.label || '';
|
|
57
58
|
this.labelElement = null;
|
|
58
59
|
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
59
|
-
this.labelFontSize = options.labelFontSize ||
|
|
60
|
+
this.labelFontSize = options.labelFontSize || DEFAULT_SHAPE_LABEL_FONT_SIZE;
|
|
60
61
|
this.labelBg = options.labelBg !== false; // labelBg:false → no knockout rect behind label
|
|
61
62
|
this.labelOffsetY = options.labelOffsetY || 0; // shift label along the perpendicular of the line, e.g. lift it clear of the stroke
|
|
62
63
|
this._isEditingLabel = false;
|
package/src/shapes/Circle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
import { registerRotationAnchor } from '../core/ScreenSpaceControls.js';
|
|
3
3
|
import { canvasToLocal, localToCanvas } from '../core/CanvasSpace.js';
|
|
4
|
+
import { DEFAULT_SHAPE_LABEL_FONT_SIZE } from '../core/ShapeLabelLayout.js';
|
|
4
5
|
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
5
6
|
// Circle shape class - extracted from drawCircle.js
|
|
6
7
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
|
|
@@ -55,7 +56,7 @@ class Circle {
|
|
|
55
56
|
this.label = options.label || '';
|
|
56
57
|
this.labelElement = null;
|
|
57
58
|
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
58
|
-
this.labelFontSize = options.labelFontSize ||
|
|
59
|
+
this.labelFontSize = options.labelFontSize || DEFAULT_SHAPE_LABEL_FONT_SIZE;
|
|
59
60
|
this._isEditingLabel = false;
|
|
60
61
|
this._hitArea = null;
|
|
61
62
|
this._labelBg = null;
|
package/src/shapes/Line.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Line shape class - extracted from lineTool.js
|
|
3
3
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom
|
|
4
4
|
import { updateAttachedArrows as updateArrowsForShape } from '../tools/arrowTool.js';
|
|
5
|
+
import { DEFAULT_SHAPE_LABEL_FONT_SIZE } from '../core/ShapeLabelLayout.js';
|
|
5
6
|
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
6
7
|
|
|
7
8
|
const rc = rough.svg(svg);
|
|
@@ -45,7 +46,7 @@ class Line {
|
|
|
45
46
|
this.label = options.label || '';
|
|
46
47
|
this.labelElement = null;
|
|
47
48
|
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
48
|
-
this.labelFontSize = options.labelFontSize ||
|
|
49
|
+
this.labelFontSize = options.labelFontSize || DEFAULT_SHAPE_LABEL_FONT_SIZE;
|
|
49
50
|
this._isEditingLabel = false;
|
|
50
51
|
this._hitArea = null;
|
|
51
52
|
this._labelBg = null;
|
package/src/shapes/Rectangle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
import { registerRotationAnchor } from '../core/ScreenSpaceControls.js';
|
|
3
3
|
import { canvasToLocal, localToCanvas } from '../core/CanvasSpace.js';
|
|
4
|
+
import { DEFAULT_SHAPE_LABEL_FONT_SIZE, measureShapeLabel } from '../core/ShapeLabelLayout.js';
|
|
4
5
|
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
5
6
|
// Rectangle shape class - extracted from drawSquare.js
|
|
6
7
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
|
|
@@ -52,7 +53,7 @@ class Rectangle {
|
|
|
52
53
|
this.label = options.label || '';
|
|
53
54
|
this.labelElement = null;
|
|
54
55
|
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
55
|
-
this.labelFontSize = options.labelFontSize ||
|
|
56
|
+
this.labelFontSize = options.labelFontSize || DEFAULT_SHAPE_LABEL_FONT_SIZE;
|
|
56
57
|
this.labelBg = options.labelBg !== false; // set labelBg:false to render plain text with no pill backdrop
|
|
57
58
|
this._isEditingLabel = false;
|
|
58
59
|
this._hitArea = null;
|
|
@@ -78,6 +79,7 @@ class Rectangle {
|
|
|
78
79
|
this.draw();
|
|
79
80
|
}
|
|
80
81
|
draw() {
|
|
82
|
+
const labelLayout = this._ensureLabelFits();
|
|
81
83
|
const childrenToRemove = [];
|
|
82
84
|
const preserveSet = this._skipAnchors ? new Set([...this.anchors, this.selectionOutline, this.rotationAnchor].filter(Boolean)) : null;
|
|
83
85
|
for (let i = 0; i < this.group.children.length; i++) {
|
|
@@ -126,7 +128,7 @@ class Rectangle {
|
|
|
126
128
|
this._updateShade();
|
|
127
129
|
|
|
128
130
|
// Update embedded label
|
|
129
|
-
this._updateLabelElement();
|
|
131
|
+
this._updateLabelElement(labelLayout);
|
|
130
132
|
|
|
131
133
|
const rotateCenterX = this.width / 2;
|
|
132
134
|
const rotateCenterY = this.height / 2;
|
|
@@ -219,7 +221,18 @@ class Rectangle {
|
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
_ensureLabelFits() {
|
|
225
|
+
if (!this.label) return null;
|
|
226
|
+
const layout = measureShapeLabel(this.label, this.labelFontSize);
|
|
227
|
+
// Dense diagram renderers opt out of the label backdrop and own their
|
|
228
|
+
// geometry. Auto-fit is for interactive rectangle labels.
|
|
229
|
+
if (this.isBeingDrawn || !this.labelBg) return layout;
|
|
230
|
+
this.width = Math.max(this.width, layout.requiredWidth);
|
|
231
|
+
this.height = Math.max(this.height, layout.requiredHeight);
|
|
232
|
+
return layout;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_updateLabelElement(layout = null) {
|
|
223
236
|
if (!this.label) {
|
|
224
237
|
// Remove label element if label is empty
|
|
225
238
|
if (this.labelElement && this.labelElement.parentNode === this.group) {
|
|
@@ -239,15 +252,25 @@ class Rectangle {
|
|
|
239
252
|
this.labelElement.setAttribute('pointer-events', 'none');
|
|
240
253
|
}
|
|
241
254
|
|
|
255
|
+
const labelLayout = layout || measureShapeLabel(this.label, this.labelFontSize);
|
|
256
|
+
const startY = (this.height - labelLayout.contentHeight) / 2;
|
|
257
|
+
|
|
258
|
+
while (this.labelElement.firstChild) this.labelElement.removeChild(this.labelElement.firstChild);
|
|
259
|
+
labelLayout.lines.forEach((line, index) => {
|
|
260
|
+
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
261
|
+
tspan.setAttribute('x', this.width / 2);
|
|
262
|
+
tspan.setAttribute('y', startY + index * labelLayout.lineHeight);
|
|
263
|
+
tspan.setAttribute('dominant-baseline', 'hanging');
|
|
264
|
+
tspan.textContent = line || ' ';
|
|
265
|
+
this.labelElement.appendChild(tspan);
|
|
266
|
+
});
|
|
267
|
+
|
|
242
268
|
// Position at center of rectangle
|
|
243
269
|
this.labelElement.setAttribute('x', this.width / 2);
|
|
244
|
-
this.labelElement.setAttribute('y', this.height / 2);
|
|
245
270
|
this.labelElement.setAttribute('text-anchor', 'middle');
|
|
246
|
-
this.labelElement.setAttribute('dominant-baseline', 'central');
|
|
247
271
|
this.labelElement.setAttribute('fill', this.labelColor);
|
|
248
272
|
this.labelElement.setAttribute('font-size', this.labelFontSize);
|
|
249
273
|
this.labelElement.setAttribute('font-family', 'lixFont, sans-serif');
|
|
250
|
-
this.labelElement.textContent = this.label;
|
|
251
274
|
|
|
252
275
|
// Background padding rect behind text (like arrows have) — skippable
|
|
253
276
|
// via labelBg:false for dense multi-row content (e.g. mermaid ER
|
|
@@ -270,9 +293,8 @@ class Rectangle {
|
|
|
270
293
|
this._labelBg.setAttribute('fill', canvasBg);
|
|
271
294
|
const hPadding = 6;
|
|
272
295
|
const vPadding = 2;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const bgH = this.labelFontSize + vPadding * 2;
|
|
296
|
+
const bgW = labelLayout.contentWidth + hPadding * 2;
|
|
297
|
+
const bgH = labelLayout.contentHeight + vPadding * 2;
|
|
276
298
|
this._labelBg.setAttribute('x', this.width / 2 - bgW / 2);
|
|
277
299
|
this._labelBg.setAttribute('y', this.height / 2 - bgH / 2);
|
|
278
300
|
this._labelBg.setAttribute('width', bgW);
|
|
@@ -363,7 +385,7 @@ class Rectangle {
|
|
|
363
385
|
}, 10);
|
|
364
386
|
|
|
365
387
|
const finishEdit = () => {
|
|
366
|
-
const newText = input.
|
|
388
|
+
const newText = input.innerText.trim().replace(/\u00A0/g, '');
|
|
367
389
|
this.label = newText;
|
|
368
390
|
this._isEditingLabel = false;
|
|
369
391
|
|
package/src/tools/eraserTool.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
// Depends on globals: svg, isEraserToolActive, ACTION_DELETE, historyStack, redoStack, clearAllSelections
|
|
4
4
|
import { createEraserTrail, updateEraserTrail, fadeOutEraserTrail, forceCleanupEraserTrail, getIsErasing, setIsErasing, getTargetedElements } from '../core/EraserTrail.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function getEraserCursorSVG() {
|
|
7
|
+
const isLight = document.documentElement.dataset.canvasTheme === 'light' || document.body?.classList.contains('theme-light');
|
|
8
|
+
const fill = isLight ? '#31283f' : '#f5effb';
|
|
9
|
+
const stroke = isLight ? '#ffffff' : '#31283f';
|
|
10
|
+
return `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="${fill}" stroke="${stroke}" stroke-width="2"/></svg>`)}`;
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
// Expose force cleanup on window so SketchEngine can call it on tool switch
|
|
9
14
|
window.forceCleanupEraserTrail = forceCleanupEraserTrail;
|
|
@@ -107,10 +112,11 @@ svg.addEventListener("pointerdown", (e) => {
|
|
|
107
112
|
|
|
108
113
|
svg.addEventListener("pointermove", (e) => {
|
|
109
114
|
if (isEraserToolActive) {
|
|
110
|
-
svg.style.cursor = `url(${
|
|
115
|
+
svg.style.cursor = `url(${getEraserCursorSVG()}) 10 10, auto`;
|
|
111
116
|
}
|
|
112
117
|
if (!getIsErasing()) return;
|
|
113
|
-
|
|
118
|
+
const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [e];
|
|
119
|
+
events.forEach((point) => updateEraserTrail(point.clientX, point.clientY));
|
|
114
120
|
handleElementHighlight(e.clientX, e.clientY);
|
|
115
121
|
});
|
|
116
122
|
|
package/src/tools/laserTool.js
CHANGED
|
@@ -12,11 +12,12 @@ const fadeOutDuration = 1200;
|
|
|
12
12
|
const baseLaserOpacity = 0.85;
|
|
13
13
|
const baseLaserWidth = 3.5;
|
|
14
14
|
const glowWidth = 10;
|
|
15
|
-
const minDistanceThreshold =
|
|
16
|
-
const smoothingWindow =
|
|
15
|
+
const minDistanceThreshold = 0.75;
|
|
16
|
+
const smoothingWindow = 5; // points to average for smoothing
|
|
17
17
|
|
|
18
18
|
let drawingAnimationId = null;
|
|
19
19
|
let fadingAnimationId = null;
|
|
20
|
+
let activePointerId = null;
|
|
20
21
|
|
|
21
22
|
function screenToViewBoxPoint(x, y) {
|
|
22
23
|
const CTM = svg.getScreenCTM();
|
|
@@ -248,7 +249,7 @@ function fadeLaserTrail(laser) {
|
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
|
|
251
|
-
svg.addEventListener("
|
|
252
|
+
svg.addEventListener("pointerdown", (e) => {
|
|
252
253
|
if (!isLaserToolActive) return;
|
|
253
254
|
|
|
254
255
|
if (e.target !== svg) {
|
|
@@ -266,6 +267,8 @@ svg.addEventListener("mousedown", (e) => {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
isDrawing = true;
|
|
270
|
+
activePointerId = e.pointerId;
|
|
271
|
+
svg.setPointerCapture?.(e.pointerId);
|
|
269
272
|
|
|
270
273
|
const laserId = "laserGroup_" + Date.now() + "_" + Math.random().toString(16).slice(2);
|
|
271
274
|
const newLaser = {
|
|
@@ -279,20 +282,23 @@ svg.addEventListener("mousedown", (e) => {
|
|
|
279
282
|
}
|
|
280
283
|
});
|
|
281
284
|
|
|
282
|
-
svg.addEventListener("
|
|
285
|
+
svg.addEventListener("pointermove", (e) => {
|
|
283
286
|
if (isLaserToolActive) {
|
|
284
287
|
svg.style.cursor = `url(${lazerCursor}) 0 0, auto`;
|
|
285
288
|
}
|
|
286
289
|
|
|
287
290
|
if (!isDrawing) return;
|
|
288
291
|
|
|
289
|
-
|
|
292
|
+
const events = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [e];
|
|
293
|
+
events.forEach((point) => addPointToCurrentLaser(point.clientX, point.clientY));
|
|
290
294
|
});
|
|
291
295
|
|
|
292
|
-
svg.addEventListener("
|
|
296
|
+
svg.addEventListener("pointerup", (e) => {
|
|
293
297
|
if (!isDrawing) return;
|
|
294
298
|
|
|
295
299
|
isDrawing = false;
|
|
300
|
+
if (activePointerId !== null) svg.releasePointerCapture?.(activePointerId);
|
|
301
|
+
activePointerId = null;
|
|
296
302
|
|
|
297
303
|
if (lasers.length > 0) {
|
|
298
304
|
fadeLaserTrail(lasers[lasers.length - 1]);
|
|
@@ -305,13 +311,13 @@ svg.addEventListener("mouseup", (e) => {
|
|
|
305
311
|
}
|
|
306
312
|
});
|
|
307
313
|
|
|
308
|
-
svg.addEventListener("
|
|
314
|
+
svg.addEventListener("pointercancel", () => {
|
|
309
315
|
if (!isDrawing) return;
|
|
310
316
|
|
|
311
317
|
isDrawing = false;
|
|
318
|
+
activePointerId = null;
|
|
312
319
|
|
|
313
320
|
if (lasers.length > 0) {
|
|
314
321
|
fadeLaserTrail(lasers[lasers.length - 1]);
|
|
315
322
|
}
|
|
316
|
-
|
|
317
323
|
});
|