@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.
Files changed (55) hide show
  1. package/dist/react/Arrow-YIOAH45F.js +9 -0
  2. package/dist/react/{Circle-5WL5HZA5.js → Circle-BZANVNWQ.js} +3 -3
  3. package/dist/react/{CopyPaste-V7AY56H3.js → CopyPaste-IY5TF5Z2.js} +6 -6
  4. package/dist/react/{EraserTrail-PA6IQLQN.js → EraserTrail-F3ZJNWNL.js} +2 -2
  5. package/dist/react/{Line-YONPLUYT.js → Line-7IGMPC5Q.js} +3 -3
  6. package/dist/react/{Rectangle-7KMA3SC6.js → Rectangle-G3MPA72T.js} +3 -3
  7. package/dist/react/{SceneSerializer-7O72LEZP.js → SceneSerializer-X5GJFI2E.js} +6 -6
  8. package/dist/react/{SketchEngine-VTWURP2K.js → SketchEngine-6UBUCW5L.js} +10 -10
  9. package/dist/react/{chunk-TCWCC4Z4.js → chunk-3ROBBYUS.js} +30 -12
  10. package/dist/react/chunk-3ROBBYUS.js.map +7 -0
  11. package/dist/react/{chunk-JYBJXTLM.js → chunk-6DJ2UFEY.js} +4 -3
  12. package/dist/react/chunk-6DJ2UFEY.js.map +7 -0
  13. package/dist/react/chunk-6KNC7RDT.js +59 -0
  14. package/dist/react/chunk-6KNC7RDT.js.map +7 -0
  15. package/dist/react/{chunk-5OUBTHHO.js → chunk-PK7MU63Q.js} +33 -12
  16. package/dist/react/chunk-PK7MU63Q.js.map +7 -0
  17. package/dist/react/{chunk-WCA3FGWE.js → chunk-YXJMIL6G.js} +4 -3
  18. package/dist/react/chunk-YXJMIL6G.js.map +7 -0
  19. package/dist/react/{chunk-VJDNKJBX.js → chunk-ZTSW4DOB.js} +4 -3
  20. package/dist/react/chunk-ZTSW4DOB.js.map +7 -0
  21. package/dist/react/{eraserTool-2GS2LQCS.js → eraserTool-P6WR35AE.js} +11 -5
  22. package/dist/react/eraserTool-P6WR35AE.js.map +7 -0
  23. package/dist/react/index.js +41 -8
  24. package/dist/react/index.js.map +3 -3
  25. package/dist/react/{laserTool-XVMV63AV.js → laserTool-L3MNCPTS.js} +15 -8
  26. package/dist/react/laserTool-L3MNCPTS.js.map +7 -0
  27. package/package.json +1 -1
  28. package/src/core/EraserTrail.js +36 -11
  29. package/src/core/ShapeLabelLayout.js +42 -0
  30. package/src/react/components/sidebars/ShapeSidebar.jsx +28 -4
  31. package/src/react/store/useSketchStore.js +5 -1
  32. package/src/shapes/Arrow.js +2 -1
  33. package/src/shapes/Circle.js +2 -1
  34. package/src/shapes/Line.js +2 -1
  35. package/src/shapes/Rectangle.js +32 -10
  36. package/src/tools/eraserTool.js +9 -3
  37. package/src/tools/laserTool.js +14 -8
  38. package/dist/react/Arrow-QMR77LGQ.js +0 -9
  39. package/dist/react/chunk-5OUBTHHO.js.map +0 -7
  40. package/dist/react/chunk-JYBJXTLM.js.map +0 -7
  41. package/dist/react/chunk-M7OYYJC6.js +0 -17
  42. package/dist/react/chunk-M7OYYJC6.js.map +0 -7
  43. package/dist/react/chunk-TCWCC4Z4.js.map +0 -7
  44. package/dist/react/chunk-VJDNKJBX.js.map +0 -7
  45. package/dist/react/chunk-WCA3FGWE.js.map +0 -7
  46. package/dist/react/eraserTool-2GS2LQCS.js.map +0 -7
  47. package/dist/react/laserTool-XVMV63AV.js.map +0 -7
  48. /package/dist/react/{Arrow-QMR77LGQ.js.map → Arrow-YIOAH45F.js.map} +0 -0
  49. /package/dist/react/{Circle-5WL5HZA5.js.map → Circle-BZANVNWQ.js.map} +0 -0
  50. /package/dist/react/{CopyPaste-V7AY56H3.js.map → CopyPaste-IY5TF5Z2.js.map} +0 -0
  51. /package/dist/react/{EraserTrail-PA6IQLQN.js.map → EraserTrail-F3ZJNWNL.js.map} +0 -0
  52. /package/dist/react/{Line-YONPLUYT.js.map → Line-7IGMPC5Q.js.map} +0 -0
  53. /package/dist/react/{Rectangle-7KMA3SC6.js.map → Rectangle-G3MPA72T.js.map} +0 -0
  54. /package/dist/react/{SceneSerializer-7O72LEZP.js.map → SceneSerializer-X5GJFI2E.js.map} +0 -0
  55. /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 = 1;
13
- var smoothingWindow = 4;
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("mousedown", (e) => {
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("mousemove", (e) => {
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
- addPointToCurrentLaser(e.clientX, e.clientY);
221
+ const events = typeof e.getCoalescedEvents === "function" ? e.getCoalescedEvents() : [e];
222
+ events.forEach((point) => addPointToCurrentLaser(point.clientX, point.clientY));
219
223
  });
220
- svg.addEventListener("mouseup", (e) => {
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("mouseleave", (e) => {
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-XVMV63AV.js.map
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.6.11",
3
+ "version": "5.6.12",
4
4
  "description": "Open-source SVG whiteboard engine + React canvas component with hand-drawn aesthetics — the core of LixSketch",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,14 +8,22 @@ let eraserGlow = null;
8
8
  let eraserPoints = [];
9
9
  let targetedElements = new Set();
10
10
 
11
- const MAX_TRAIL_LENGTH = 16;
12
- const FADE_DURATION = 200;
13
- const MIN_POINT_DISTANCE = 1.5;
14
- const SMOOTHING_WINDOW = 3;
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
- eraserGlow.setAttribute("stroke", "rgba(80, 80, 80, 0.15)");
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", "rgba(53, 53, 53, 0.5)");
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
- renderTrail(eraserPoints, 1.0);
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.5 * opacityMul;
172
- const glowOpacity = 0.15 * opacityMul;
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(53, 53, 53, ${coreOpacity})`);
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(80, 80, 80, ${glowOpacity})`);
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 && !ref.current.contains(e.target)) setOpen(false)
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 className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 z-20">
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
- return `url("data:image/svg+xml;base64,${typeof btoa !== 'undefined' ? 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>') : ''}") 10 10, auto`
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`
@@ -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 || 12;
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;
@@ -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 || 14;
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;
@@ -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 || 12;
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;
@@ -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 || 14;
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
- _updateLabelElement() {
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 charWidth = this.labelFontSize * 0.6;
274
- const bgW = this.label.length * charWidth + hPadding * 2;
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.textContent.trim().replace(/\u00A0/g, '');
388
+ const newText = input.innerText.trim().replace(/\u00A0/g, '');
367
389
  this.label = newText;
368
390
  this._isEditingLabel = false;
369
391
 
@@ -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
- 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>')}`;
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(${eraserCursorSVG}) 10 10, auto`;
115
+ svg.style.cursor = `url(${getEraserCursorSVG()}) 10 10, auto`;
111
116
  }
112
117
  if (!getIsErasing()) return;
113
- updateEraserTrail(e.clientX, e.clientY);
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
 
@@ -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 = 1;
16
- const smoothingWindow = 4; // points to average for smoothing
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("mousedown", (e) => {
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("mousemove", (e) => {
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
- addPointToCurrentLaser(e.clientX, e.clientY);
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("mouseup", (e) => {
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("mouseleave", (e) => {
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
  });
@@ -1,9 +0,0 @@
1
- "use client";
2
- import {
3
- Arrow
4
- } from "./chunk-WCA3FGWE.js";
5
- import "./chunk-M7OYYJC6.js";
6
- export {
7
- Arrow
8
- };
9
- //# sourceMappingURL=Arrow-QMR77LGQ.js.map