@elixpo/lixsketch 5.5.19 → 5.5.21
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/Circle-6POZCUZD.js +9 -0
- package/dist/react/{CopyPaste-JMIXWUUO.js → CopyPaste-FJN7K5LS.js} +43 -14
- package/dist/react/CopyPaste-FJN7K5LS.js.map +7 -0
- package/dist/react/{EventDispatcher-H5EA4KZN.js → EventDispatcher-O7HCQQNM.js} +14 -13
- package/dist/react/{EventDispatcher-H5EA4KZN.js.map → EventDispatcher-O7HCQQNM.js.map} +1 -1
- package/dist/react/Frame-RLSMJJSK.js +11 -0
- package/dist/react/FreehandStroke-U6GYZOM4.js +9 -0
- package/dist/react/{GraphEngine-KQXHNTZQ.js → GraphEngine-MM6ALA2N.js} +105 -148
- package/dist/react/{GraphEngine-KQXHNTZQ.js.map → GraphEngine-MM6ALA2N.js.map} +2 -2
- package/dist/react/IconShape-7OPXAKLK.js +10 -0
- package/dist/react/ImageShape-LRNJQCF3.js +10 -0
- package/dist/react/Line-WWQFBNX5.js +10 -0
- package/dist/react/Rectangle-S7XUKWYD.js +9 -0
- package/dist/react/{SceneSerializer-ARTKPZVR.js → SceneSerializer-GSJR7ZHC.js} +36 -11
- package/dist/react/SceneSerializer-GSJR7ZHC.js.map +7 -0
- package/dist/react/{Selection-YJNZRBON.js → Selection-ZL3G6IAO.js} +5 -4
- package/dist/react/{SketchEngine-ZGYBIZG7.js → SketchEngine-BFLHJPCG.js} +25 -25
- package/dist/react/{UndoRedo-BZ6W4QTV.js → UndoRedo-NL7KLR5B.js} +2 -2
- package/dist/react/{ZoomPan-NYPDJBD5.js → ZoomPan-FAWA5OPS.js} +5 -1
- package/dist/react/ZoomPan-FAWA5OPS.js.map +7 -0
- package/dist/react/{arrowTool-LKFSZKXC.js → arrowTool-CNVGM6RX.js} +3 -3
- package/dist/react/{chunk-RFHMFWVR.js → chunk-2FMFOPGW.js} +5 -6
- package/dist/react/{chunk-RFHMFWVR.js.map → chunk-2FMFOPGW.js.map} +2 -2
- package/dist/react/{chunk-QYNTPZON.js → chunk-2THKDSWW.js} +9 -5
- package/dist/react/chunk-2THKDSWW.js.map +7 -0
- package/dist/react/{chunk-JOPQ27YH.js → chunk-4BLMNGTR.js} +3 -3
- package/dist/react/{chunk-C62SXFBL.js → chunk-4TAS67SL.js} +23 -22
- package/dist/react/chunk-4TAS67SL.js.map +7 -0
- package/dist/react/{chunk-FV3EPR4L.js → chunk-AH4PWNEB.js} +19 -11
- package/dist/react/chunk-AH4PWNEB.js.map +7 -0
- package/dist/react/{chunk-XCJOF6BM.js → chunk-FETTJLCV.js} +5 -1
- package/dist/react/chunk-FETTJLCV.js.map +7 -0
- package/dist/react/{chunk-JJTII2UQ.js → chunk-GC3KTQDA.js} +78 -15
- package/dist/react/chunk-GC3KTQDA.js.map +7 -0
- package/dist/react/{chunk-2V7G2PLX.js → chunk-GPIYXREE.js} +3 -3
- package/dist/react/{chunk-3VSGFP6U.js → chunk-HFQZYCHJ.js} +8 -4
- package/dist/react/chunk-HFQZYCHJ.js.map +7 -0
- package/dist/react/{chunk-C72OHQLE.js → chunk-ILGAFR56.js} +3 -3
- package/dist/react/{chunk-CF7AK4ES.js → chunk-KVAQPCUH.js} +26 -3
- package/dist/react/chunk-KVAQPCUH.js.map +7 -0
- package/dist/react/{chunk-WEWMB35W.js → chunk-OFGJJKPH.js} +3 -3
- package/dist/react/chunk-PAKA6HUX.js +59 -0
- package/dist/react/chunk-PAKA6HUX.js.map +7 -0
- package/dist/react/{chunk-HKYUJUD7.js → chunk-PXFPPO3R.js} +5 -1
- package/dist/react/chunk-PXFPPO3R.js.map +7 -0
- package/dist/react/{chunk-6G3ITAS5.js → chunk-SZD7DKYJ.js} +18 -9
- package/dist/react/chunk-SZD7DKYJ.js.map +7 -0
- package/dist/react/{chunk-N7NZLF6P.js → chunk-TKXKJNTX.js} +5 -1
- package/dist/react/chunk-TKXKJNTX.js.map +7 -0
- package/dist/react/{chunk-MYYBFZD3.js → chunk-U3TB4KRS.js} +45 -6
- package/dist/react/chunk-U3TB4KRS.js.map +7 -0
- package/dist/react/{chunk-UQBT7F4K.js → chunk-V2QTYSBK.js} +2 -2
- package/dist/react/{chunk-73VA5ALZ.js → chunk-XFQECTTF.js} +48 -19
- package/dist/react/chunk-XFQECTTF.js.map +7 -0
- package/dist/react/{chunk-O64N5JPO.js → chunk-YF3TK7SC.js} +2 -2
- package/dist/react/{circleTool-RDOIMKKV.js → circleTool-KICNRQCI.js} +4 -4
- package/dist/react/{codeTool-O2Z7ZXGP.js → codeTool-NCQ73R6M.js} +5 -4
- package/dist/react/{frameTool-Z5ZDCX6H.js → frameTool-Y6KB67DO.js} +4 -4
- package/dist/react/{freehandTool-AAQCTMVM.js → freehandTool-MMULHCQ7.js} +4 -4
- package/dist/react/{iconTool-AUHRLOO2.js → iconTool-LSW2DYGB.js} +5 -4
- package/dist/react/{imageTool-DRKDCPUV.js → imageTool-2DSR32RX.js} +5 -4
- package/dist/react/index.js +141 -66
- package/dist/react/index.js.map +3 -3
- package/dist/react/{lineTool-XLOOZE3E.js → lineTool-N5I7IGT7.js} +4 -4
- package/dist/react/{rectangleTool-AEYDDJCA.js → rectangleTool-YUT7ZK2E.js} +4 -4
- package/dist/react/{textTool-UORBAAJE.js → textTool-IQOYPELP.js} +6 -5
- package/package.json +1 -1
- package/src/core/CopyPaste.js +34 -4
- package/src/core/GraphEngine.js +129 -185
- package/src/core/SceneSerializer.js +26 -0
- package/src/core/ScreenSpaceControls.js +60 -0
- package/src/core/Selection.js +19 -11
- package/src/core/UndoRedo.js +47 -3
- package/src/core/ZoomPan.js +3 -0
- package/src/react/components/canvas/MultiSelectActions.jsx +4 -1
- package/src/react/components/sidebars/IconSidebar.jsx +14 -23
- package/src/react/components/sidebars/ImageSidebar.jsx +47 -24
- package/src/react/store/useUIStore.js +71 -14
- package/src/shapes/Circle.js +2 -0
- package/src/shapes/Frame.js +26 -2
- package/src/shapes/FreehandStroke.js +2 -0
- package/src/shapes/IconShape.js +23 -25
- package/src/shapes/ImageShape.js +6 -2
- package/src/shapes/Rectangle.js +2 -0
- package/src/tools/codeTool.js +3 -2
- package/src/tools/frameTool.js +19 -9
- package/src/tools/iconTool.js +63 -26
- package/src/tools/imageTool.js +80 -14
- package/src/tools/textTool.js +3 -1
- package/dist/react/Circle-QQMKCKFR.js +0 -8
- package/dist/react/CopyPaste-JMIXWUUO.js.map +0 -7
- package/dist/react/Frame-VADA625S.js +0 -10
- package/dist/react/FreehandStroke-BOWDNAVC.js +0 -8
- package/dist/react/IconShape-32EYI2MA.js +0 -10
- package/dist/react/ImageShape-MYP2DVVH.js +0 -10
- package/dist/react/Line-FCCF2BOB.js +0 -10
- package/dist/react/Rectangle-MZ6ZB6KM.js +0 -8
- package/dist/react/SceneSerializer-ARTKPZVR.js.map +0 -7
- package/dist/react/ZoomPan-NYPDJBD5.js.map +0 -7
- package/dist/react/chunk-3VSGFP6U.js.map +0 -7
- package/dist/react/chunk-6G3ITAS5.js.map +0 -7
- package/dist/react/chunk-73VA5ALZ.js.map +0 -7
- package/dist/react/chunk-C62SXFBL.js.map +0 -7
- package/dist/react/chunk-CF7AK4ES.js.map +0 -7
- package/dist/react/chunk-FV3EPR4L.js.map +0 -7
- package/dist/react/chunk-HKYUJUD7.js.map +0 -7
- package/dist/react/chunk-JJTII2UQ.js.map +0 -7
- package/dist/react/chunk-MYYBFZD3.js.map +0 -7
- package/dist/react/chunk-N7NZLF6P.js.map +0 -7
- package/dist/react/chunk-QYNTPZON.js.map +0 -7
- package/dist/react/chunk-XCJOF6BM.js.map +0 -7
- /package/dist/react/{Circle-QQMKCKFR.js.map → Circle-6POZCUZD.js.map} +0 -0
- /package/dist/react/{Frame-VADA625S.js.map → Frame-RLSMJJSK.js.map} +0 -0
- /package/dist/react/{FreehandStroke-BOWDNAVC.js.map → FreehandStroke-U6GYZOM4.js.map} +0 -0
- /package/dist/react/{IconShape-32EYI2MA.js.map → IconShape-7OPXAKLK.js.map} +0 -0
- /package/dist/react/{ImageShape-MYP2DVVH.js.map → ImageShape-LRNJQCF3.js.map} +0 -0
- /package/dist/react/{Line-FCCF2BOB.js.map → Line-WWQFBNX5.js.map} +0 -0
- /package/dist/react/{Rectangle-MZ6ZB6KM.js.map → Rectangle-S7XUKWYD.js.map} +0 -0
- /package/dist/react/{Selection-YJNZRBON.js.map → Selection-ZL3G6IAO.js.map} +0 -0
- /package/dist/react/{SketchEngine-ZGYBIZG7.js.map → SketchEngine-BFLHJPCG.js.map} +0 -0
- /package/dist/react/{UndoRedo-BZ6W4QTV.js.map → UndoRedo-NL7KLR5B.js.map} +0 -0
- /package/dist/react/{arrowTool-LKFSZKXC.js.map → arrowTool-CNVGM6RX.js.map} +0 -0
- /package/dist/react/{chunk-JOPQ27YH.js.map → chunk-4BLMNGTR.js.map} +0 -0
- /package/dist/react/{chunk-2V7G2PLX.js.map → chunk-GPIYXREE.js.map} +0 -0
- /package/dist/react/{chunk-C72OHQLE.js.map → chunk-ILGAFR56.js.map} +0 -0
- /package/dist/react/{chunk-WEWMB35W.js.map → chunk-OFGJJKPH.js.map} +0 -0
- /package/dist/react/{chunk-UQBT7F4K.js.map → chunk-V2QTYSBK.js.map} +0 -0
- /package/dist/react/{chunk-O64N5JPO.js.map → chunk-YF3TK7SC.js.map} +0 -0
- /package/dist/react/{circleTool-RDOIMKKV.js.map → circleTool-KICNRQCI.js.map} +0 -0
- /package/dist/react/{codeTool-O2Z7ZXGP.js.map → codeTool-NCQ73R6M.js.map} +0 -0
- /package/dist/react/{frameTool-Z5ZDCX6H.js.map → frameTool-Y6KB67DO.js.map} +0 -0
- /package/dist/react/{freehandTool-AAQCTMVM.js.map → freehandTool-MMULHCQ7.js.map} +0 -0
- /package/dist/react/{iconTool-AUHRLOO2.js.map → iconTool-LSW2DYGB.js.map} +0 -0
- /package/dist/react/{imageTool-DRKDCPUV.js.map → imageTool-2DSR32RX.js.map} +0 -0
- /package/dist/react/{lineTool-XLOOZE3E.js.map → lineTool-N5I7IGT7.js.map} +0 -0
- /package/dist/react/{rectangleTool-AEYDDJCA.js.map → rectangleTool-YUT7ZK2E.js.map} +0 -0
- /package/dist/react/{textTool-UORBAAJE.js.map → textTool-IQOYPELP.js.map} +0 -0
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
handleMouseDown,
|
|
4
4
|
handleMouseMove,
|
|
5
5
|
handleMouseUp
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-ILGAFR56.js";
|
|
7
|
+
import "./chunk-YF3TK7SC.js";
|
|
8
|
+
import "./chunk-U3TB4KRS.js";
|
|
9
9
|
export {
|
|
10
10
|
handleMouseDown as handleMouseDownLine,
|
|
11
11
|
handleMouseMove as handleMouseMoveLine,
|
|
12
12
|
handleMouseUp as handleMouseUpLine
|
|
13
13
|
};
|
|
14
|
-
//# sourceMappingURL=lineTool-
|
|
14
|
+
//# sourceMappingURL=lineTool-N5I7IGT7.js.map
|
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
handleMouseDownRect,
|
|
4
4
|
handleMouseMoveRect,
|
|
5
5
|
handleMouseUpRect
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-4BLMNGTR.js";
|
|
7
|
+
import "./chunk-YF3TK7SC.js";
|
|
8
|
+
import "./chunk-U3TB4KRS.js";
|
|
9
9
|
export {
|
|
10
10
|
handleMouseDownRect,
|
|
11
11
|
handleMouseMoveRect,
|
|
12
12
|
handleMouseUpRect
|
|
13
13
|
};
|
|
14
|
-
//# sourceMappingURL=rectangleTool-
|
|
14
|
+
//# sourceMappingURL=rectangleTool-YUT7ZK2E.js.map
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
handleTextMouseMove,
|
|
7
7
|
handleTextMouseUp,
|
|
8
8
|
updateCodeToggleForShape
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-2THKDSWW.js";
|
|
10
|
+
import "./chunk-HFQZYCHJ.js";
|
|
11
|
+
import "./chunk-PAKA6HUX.js";
|
|
12
|
+
import "./chunk-YF3TK7SC.js";
|
|
13
|
+
import "./chunk-U3TB4KRS.js";
|
|
13
14
|
export {
|
|
14
15
|
deselectElement as deselectTextElement,
|
|
15
16
|
enterEditMode,
|
|
@@ -18,4 +19,4 @@ export {
|
|
|
18
19
|
handleTextMouseUp,
|
|
19
20
|
updateCodeToggleForShape
|
|
20
21
|
};
|
|
21
|
-
//# sourceMappingURL=textTool-
|
|
22
|
+
//# sourceMappingURL=textTool-IQOYPELP.js.map
|
package/package.json
CHANGED
package/src/core/CopyPaste.js
CHANGED
|
@@ -147,7 +147,13 @@ function serializeShape(shape) {
|
|
|
147
147
|
width: shape.width,
|
|
148
148
|
height: shape.height,
|
|
149
149
|
rotation: shape.rotation,
|
|
150
|
-
href: el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || ''
|
|
150
|
+
href: el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || '',
|
|
151
|
+
storageProvider: el.getAttribute('data-storage-provider') || null,
|
|
152
|
+
storageCloudName: el.getAttribute('data-storage-cloud-name') || null,
|
|
153
|
+
frameType: shape._frameType || null,
|
|
154
|
+
graphData: shape._frameType === 'graph' && shape._graphData
|
|
155
|
+
? cloneOptions(shape._graphData)
|
|
156
|
+
: null
|
|
151
157
|
};
|
|
152
158
|
}
|
|
153
159
|
|
|
@@ -383,11 +389,22 @@ function createShapeFromData(data, offsetX, offsetY) {
|
|
|
383
389
|
imgEl.setAttribute('data-shape-height', data.height);
|
|
384
390
|
imgEl.setAttribute('preserveAspectRatio', 'none');
|
|
385
391
|
imgEl.setAttribute('style', 'cursor: pointer;');
|
|
392
|
+
if (data.storageProvider) imgEl.setAttribute('data-storage-provider', data.storageProvider);
|
|
393
|
+
if (data.storageCloudName) imgEl.setAttribute('data-storage-cloud-name', data.storageCloudName);
|
|
386
394
|
|
|
387
395
|
// ImageShape constructor moves element into a group and appends to svg
|
|
388
396
|
svgEl.appendChild(imgEl);
|
|
389
397
|
const imageShape = new ImageShape(imgEl);
|
|
390
398
|
imageShape.rotation = data.rotation;
|
|
399
|
+
if (data.frameType === 'graph' && data.graphData) {
|
|
400
|
+
if (typeof window.__hydrateGraphShape === 'function') {
|
|
401
|
+
window.__hydrateGraphShape(imageShape, data.graphData);
|
|
402
|
+
} else {
|
|
403
|
+
imageShape._frameType = 'graph';
|
|
404
|
+
imageShape._graphData = cloneOptions(data.graphData);
|
|
405
|
+
imageShape.element.setAttribute('data-graph-shape', 'true');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
391
408
|
return imageShape;
|
|
392
409
|
}
|
|
393
410
|
|
|
@@ -605,15 +622,25 @@ function handlePasteEvent(e) {
|
|
|
605
622
|
const rawDataUrl = ev.target.result;
|
|
606
623
|
const isSvg = (blob.type || '').toLowerCase() === 'image/svg+xml';
|
|
607
624
|
let placedDataUrl = rawDataUrl;
|
|
625
|
+
let placedSize = blob.size;
|
|
608
626
|
if (!isSvg) {
|
|
609
627
|
try {
|
|
610
628
|
const compressed = await compressImage(rawDataUrl);
|
|
611
|
-
if (compressed?.dataUrl)
|
|
629
|
+
if (compressed?.dataUrl) {
|
|
630
|
+
placedDataUrl = compressed.dataUrl;
|
|
631
|
+
placedSize = compressed.compressedSize || placedSize;
|
|
632
|
+
}
|
|
612
633
|
} catch (err) {
|
|
613
634
|
console.warn('[CopyPaste] Pre-placement compression failed, using raw:', err);
|
|
614
635
|
}
|
|
615
636
|
}
|
|
616
|
-
|
|
637
|
+
const limit = Number(window.__roomImageLimitBytes) || 2 * 1024 * 1024;
|
|
638
|
+
const personalStorage = window.__personalCloudinary?.connected && window.__personalCloudinary?.useForUploads;
|
|
639
|
+
if (!personalStorage && (window.__roomImageBytesUsed || 0) + placedSize > limit) {
|
|
640
|
+
alert(`Workspace image limit reached (${Math.round(limit / (1024 * 1024))} MB).`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
placeImageFromDataUrl(placedDataUrl, placedSize);
|
|
617
644
|
};
|
|
618
645
|
reader.readAsDataURL(blob);
|
|
619
646
|
return; // only handle first image
|
|
@@ -621,7 +648,7 @@ function handlePasteEvent(e) {
|
|
|
621
648
|
}
|
|
622
649
|
}
|
|
623
650
|
|
|
624
|
-
function placeImageFromDataUrl(dataUrl) {
|
|
651
|
+
function placeImageFromDataUrl(dataUrl, sizeBytes = 0) {
|
|
625
652
|
const svgEl = getSVGElement();
|
|
626
653
|
if (!svgEl || !window.ImageShape) return;
|
|
627
654
|
|
|
@@ -648,6 +675,9 @@ function placeImageFromDataUrl(dataUrl) {
|
|
|
648
675
|
imgEl.setAttribute('data-shape-height', displayH);
|
|
649
676
|
imgEl.setAttribute('type', 'image');
|
|
650
677
|
imgEl.setAttribute('preserveAspectRatio', 'none');
|
|
678
|
+
imgEl.__fileSize = sizeBytes;
|
|
679
|
+
imgEl.setAttribute('data-file-size', String(sizeBytes));
|
|
680
|
+
window.__roomImageBytesUsed = (window.__roomImageBytesUsed || 0) + sizeBytes;
|
|
651
681
|
|
|
652
682
|
svgEl.appendChild(imgEl);
|
|
653
683
|
const imageShape = new ImageShape(imgEl);
|
package/src/core/GraphEngine.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
/**
|
|
3
|
-
* GraphEngine -
|
|
4
|
-
*
|
|
3
|
+
* GraphEngine - bridge between graph rendering and the sketch canvas.
|
|
4
|
+
*
|
|
5
|
+
* Canvas graphs use ImageShape with an SVG data URL. This keeps the graph a
|
|
6
|
+
* single crisp vector element while reusing the engine's established select,
|
|
7
|
+
* translate, resize, rotate, copy/paste, and frame-containment behaviour.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import { parseExpression, isValidExpression } from './GraphMathParser.js';
|
|
@@ -10,209 +13,150 @@ import { renderGraphSVG, renderGraphPreviewSVG, GRAPH_COLORS } from './GraphRend
|
|
|
10
13
|
const NS = 'http://www.w3.org/2000/svg';
|
|
11
14
|
const GRAPH_WIDTH = 600;
|
|
12
15
|
const GRAPH_HEIGHT = 420;
|
|
16
|
+
const MAX_PLACED_SCREEN_WIDTH = 480;
|
|
17
|
+
const MAX_VIEWPORT_WIDTH_RATIO = 0.68;
|
|
18
|
+
const MAX_VIEWPORT_HEIGHT_RATIO = 0.58;
|
|
19
|
+
|
|
20
|
+
function cloneGraphData(equations, settings) {
|
|
21
|
+
return {
|
|
22
|
+
equations: (equations || []).map((equation) => ({
|
|
23
|
+
expression: equation.expression,
|
|
24
|
+
color: equation.color,
|
|
25
|
+
})),
|
|
26
|
+
settings: { ...(settings || {}) },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function graphSvgDataUrl(svgMarkup) {
|
|
31
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function refreshGraphImage(shape) {
|
|
35
|
+
if (!shape?._graphData || !shape.element) return false;
|
|
36
|
+
const svgMarkup = renderGraphSVG(shape._graphData.equations, {
|
|
37
|
+
...shape._graphData.settings,
|
|
38
|
+
width: GRAPH_WIDTH,
|
|
39
|
+
height: GRAPH_HEIGHT,
|
|
40
|
+
});
|
|
41
|
+
if (!svgMarkup) return false;
|
|
42
|
+
const href = graphSvgDataUrl(svgMarkup);
|
|
43
|
+
shape.element.setAttribute('href', href);
|
|
44
|
+
shape.element.setAttribute('data-href', href);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Attach graph behaviour to a regular ImageShape, including restored and
|
|
49
|
+
* pasted graphs. The function is intentionally idempotent. */
|
|
50
|
+
function hydrateGraphShape(shape, graphData) {
|
|
51
|
+
if (!shape?.element || !graphData) return shape;
|
|
52
|
+
shape._frameType = 'graph';
|
|
53
|
+
shape._graphData = cloneGraphData(graphData.equations, graphData.settings);
|
|
54
|
+
shape.uploadStatus = 'done';
|
|
55
|
+
shape.element.setAttribute('data-graph-shape', 'true');
|
|
56
|
+
shape.element.setAttribute('preserveAspectRatio', 'none');
|
|
57
|
+
shape.element.style.cursor = 'move';
|
|
58
|
+
shape._refreshGraphImage = () => refreshGraphImage(shape);
|
|
59
|
+
shape.adaptToBackground = () => refreshGraphImage(shape);
|
|
60
|
+
return shape;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selectOnlyGraph(shape) {
|
|
64
|
+
if (!shape) return;
|
|
65
|
+
|
|
66
|
+
if (window.multiSelection?.selectedShapes?.size) {
|
|
67
|
+
window.multiSelection.clearSelection();
|
|
68
|
+
}
|
|
69
|
+
if (window.currentShape && window.currentShape !== shape
|
|
70
|
+
&& typeof window.currentShape.removeSelection === 'function') {
|
|
71
|
+
window.currentShape.removeSelection();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generated content always lands ready to manipulate. Use the store bridge
|
|
75
|
+
// so the toolbar and legacy engine flags change together.
|
|
76
|
+
window.__sketchStoreApi?.setActiveTool?.('select');
|
|
77
|
+
window.currentShape = shape;
|
|
78
|
+
shape.selectShape?.();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function placedGraphSize(viewBox) {
|
|
82
|
+
const zoom = Math.max(0.001, Number(window.currentZoom) || 1);
|
|
83
|
+
const byScreen = MAX_PLACED_SCREEN_WIDTH / zoom;
|
|
84
|
+
const byViewportWidth = viewBox.width * MAX_VIEWPORT_WIDTH_RATIO;
|
|
85
|
+
const byViewportHeight = viewBox.height * MAX_VIEWPORT_HEIGHT_RATIO * (GRAPH_WIDTH / GRAPH_HEIGHT);
|
|
86
|
+
const width = Math.max(80 / zoom, Math.min(GRAPH_WIDTH, byScreen, byViewportWidth, byViewportHeight));
|
|
87
|
+
return { width, height: width * (GRAPH_HEIGHT / GRAPH_WIDTH) };
|
|
88
|
+
}
|
|
13
89
|
|
|
14
90
|
/**
|
|
15
|
-
* Place a graph
|
|
91
|
+
* Place a graph as one editable vector-image shape, or update an existing
|
|
92
|
+
* graph in place without losing its canvas position, size, or rotation.
|
|
16
93
|
*/
|
|
17
|
-
function renderGraphOnCanvas(equations, settings) {
|
|
18
|
-
|
|
19
|
-
if (
|
|
94
|
+
function renderGraphOnCanvas(equations, settings, existingShape = null) {
|
|
95
|
+
const validEquations = (equations || []).filter((equation) => equation?.expression?.trim());
|
|
96
|
+
if (validEquations.length === 0) return false;
|
|
97
|
+
if (!window.svg || !window.ImageShape) {
|
|
20
98
|
console.error('[GraphEngine] Engine not initialized');
|
|
21
99
|
return false;
|
|
22
100
|
}
|
|
23
101
|
|
|
24
|
-
|
|
25
|
-
const svgMarkup = renderGraphSVG(equations, {
|
|
26
|
-
...settings,
|
|
27
|
-
width: GRAPH_WIDTH,
|
|
28
|
-
height: GRAPH_HEIGHT,
|
|
29
|
-
});
|
|
30
|
-
if (!svgMarkup) return false;
|
|
102
|
+
const graphData = cloneGraphData(validEquations, settings);
|
|
31
103
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Build title from equations
|
|
43
|
-
const eqLabels = equations
|
|
44
|
-
.filter(eq => eq.expression && eq.expression.trim())
|
|
45
|
-
.map(eq => eq.expression.trim())
|
|
46
|
-
.slice(0, 3);
|
|
47
|
-
const title = eqLabels.length > 0
|
|
48
|
-
? 'Graph: ' + eqLabels.join(', ')
|
|
49
|
-
: 'Graph';
|
|
50
|
-
|
|
51
|
-
// Phase 5 (issue #22): no wrapper frame. The graph is conceptually a
|
|
52
|
-
// single rendering — axes, grid, curves all interlocked — so we don't
|
|
53
|
-
// split it into per-curve shapes. Instead the graph becomes ONE
|
|
54
|
-
// first-class shape on the canvas: a `graphShape` that implements the
|
|
55
|
-
// shape API the engine relies on (contains / selectShape / etc.) so
|
|
56
|
-
// it can be selected, dragged, attached to by arrows, picked up by
|
|
57
|
-
// the rect-drag — exactly like a user-drawn shape.
|
|
58
|
-
try {
|
|
59
|
-
const parser = new DOMParser();
|
|
60
|
-
const doc = parser.parseFromString(svgMarkup, 'image/svg+xml');
|
|
61
|
-
const svgEl = doc.querySelector('svg');
|
|
62
|
-
if (!svgEl) return false;
|
|
63
|
-
|
|
64
|
-
const graphGroup = document.createElementNS(NS, 'g');
|
|
65
|
-
graphGroup.setAttribute('data-type', 'graph-group');
|
|
66
|
-
graphGroup.setAttribute('transform', `translate(${frameX + 20}, ${frameY + 20})`);
|
|
67
|
-
|
|
68
|
-
// Copy defs (clip paths)
|
|
69
|
-
const defs = svgEl.querySelector('defs');
|
|
70
|
-
if (defs) {
|
|
71
|
-
const defsClone = defs.cloneNode(true);
|
|
72
|
-
window.svg.querySelector('defs')?.appendChild(defsClone.firstChild) ||
|
|
73
|
-
window.svg.insertBefore(defsClone, window.svg.firstChild);
|
|
74
|
-
}
|
|
104
|
+
if (existingShape?._frameType === 'graph' && existingShape.element) {
|
|
105
|
+
hydrateGraphShape(existingShape, graphData);
|
|
106
|
+
if (!refreshGraphImage(existingShape)) return false;
|
|
107
|
+
selectOnlyGraph(existingShape);
|
|
108
|
+
window.__adaptCanvasContrast?.(
|
|
109
|
+
window.getComputedStyle(window.svg).backgroundColor || '#15111f'
|
|
110
|
+
);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
75
113
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
114
|
+
const viewBox = window.currentViewBox || {
|
|
115
|
+
x: 0,
|
|
116
|
+
y: 0,
|
|
117
|
+
width: window.innerWidth,
|
|
118
|
+
height: window.innerHeight,
|
|
119
|
+
};
|
|
120
|
+
const size = placedGraphSize(viewBox);
|
|
121
|
+
const x = viewBox.x + (viewBox.width - size.width) / 2;
|
|
122
|
+
const y = viewBox.y + (viewBox.height - size.height) / 2;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const image = document.createElementNS(NS, 'image');
|
|
126
|
+
image.setAttribute('x', x);
|
|
127
|
+
image.setAttribute('y', y);
|
|
128
|
+
image.setAttribute('width', size.width);
|
|
129
|
+
image.setAttribute('height', size.height);
|
|
130
|
+
image.setAttribute('data-shape-x', x);
|
|
131
|
+
image.setAttribute('data-shape-y', y);
|
|
132
|
+
image.setAttribute('data-shape-width', size.width);
|
|
133
|
+
image.setAttribute('data-shape-height', size.height);
|
|
134
|
+
|
|
135
|
+
const graphShape = hydrateGraphShape(new window.ImageShape(image), graphData);
|
|
136
|
+
if (!refreshGraphImage(graphShape)) {
|
|
137
|
+
graphShape.group?.remove();
|
|
138
|
+
return false;
|
|
80
139
|
}
|
|
81
|
-
window.svg.appendChild(graphGroup);
|
|
82
|
-
|
|
83
|
-
const graphShape = {
|
|
84
|
-
shapeName: 'graph', // first-class shapeName
|
|
85
|
-
shapeID: `graph-${Date.now().toString(36)}-${Math.floor(Math.random() * 10000)}`,
|
|
86
|
-
group: graphGroup,
|
|
87
|
-
element: graphGroup,
|
|
88
|
-
x: frameX + 20,
|
|
89
|
-
y: frameY + 20,
|
|
90
|
-
width: GRAPH_WIDTH,
|
|
91
|
-
height: GRAPH_HEIGHT,
|
|
92
|
-
rotation: 0,
|
|
93
|
-
isSelected: false,
|
|
94
|
-
_selectionRect: null,
|
|
95
|
-
_graphData: {
|
|
96
|
-
equations: equations.map(eq => ({ expression: eq.expression, color: eq.color })),
|
|
97
|
-
settings: { ...settings },
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
// ── Shape API the engine relies on ────────────────────────
|
|
101
|
-
contains(px, py) {
|
|
102
|
-
return px >= this.x && px <= this.x + this.width
|
|
103
|
-
&& py >= this.y && py <= this.y + this.height;
|
|
104
|
-
},
|
|
105
|
-
move(dx, dy) {
|
|
106
|
-
this.x += dx; this.y += dy;
|
|
107
|
-
this.group.setAttribute('transform', `translate(${this.x}, ${this.y})`);
|
|
108
|
-
this._updateSelectionRect();
|
|
109
|
-
},
|
|
110
|
-
selectShape() {
|
|
111
|
-
this.isSelected = true;
|
|
112
|
-
if (this._selectionRect) return;
|
|
113
|
-
const r = document.createElementNS(NS, 'rect');
|
|
114
|
-
r.setAttribute('fill', 'none');
|
|
115
|
-
r.setAttribute('stroke', '#9b7bf7');
|
|
116
|
-
r.setAttribute('stroke-width', '1.5');
|
|
117
|
-
r.setAttribute('stroke-dasharray', '4 3');
|
|
118
|
-
r.setAttribute('pointer-events', 'none');
|
|
119
|
-
window.svg.appendChild(r);
|
|
120
|
-
this._selectionRect = r;
|
|
121
|
-
this._updateSelectionRect();
|
|
122
|
-
},
|
|
123
|
-
removeSelection() {
|
|
124
|
-
this.isSelected = false;
|
|
125
|
-
if (this._selectionRect && this._selectionRect.parentNode) {
|
|
126
|
-
this._selectionRect.parentNode.removeChild(this._selectionRect);
|
|
127
|
-
}
|
|
128
|
-
this._selectionRect = null;
|
|
129
|
-
},
|
|
130
|
-
_updateSelectionRect() {
|
|
131
|
-
if (!this._selectionRect) return;
|
|
132
|
-
const pad = 4;
|
|
133
|
-
this._selectionRect.setAttribute('x', this.x - pad);
|
|
134
|
-
this._selectionRect.setAttribute('y', this.y - pad);
|
|
135
|
-
this._selectionRect.setAttribute('width', this.width + pad * 2);
|
|
136
|
-
this._selectionRect.setAttribute('height', this.height + pad * 2);
|
|
137
|
-
},
|
|
138
|
-
updateAttachedArrows() {
|
|
139
|
-
if (typeof window.updateAttachedArrows === 'function') {
|
|
140
|
-
window.updateAttachedArrows(this);
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
adaptToBackground(background, foreground, muted) {
|
|
144
|
-
const ensureContrast = window.__ensureCanvasContrast || ((color) => color);
|
|
145
|
-
|
|
146
|
-
this.group.querySelectorAll('rect').forEach((rect) => {
|
|
147
|
-
const fill = rect.getAttribute('fill');
|
|
148
|
-
const stroke = rect.getAttribute('stroke');
|
|
149
|
-
if (fill && fill !== 'none') rect.setAttribute('fill', background);
|
|
150
|
-
if (stroke && stroke !== 'none') {
|
|
151
|
-
rect.setAttribute('stroke', muted);
|
|
152
|
-
rect.setAttribute('stroke-opacity', '0.45');
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
this.group.querySelectorAll('line').forEach((line) => {
|
|
157
|
-
const width = Number.parseFloat(line.getAttribute('stroke-width') || '1');
|
|
158
|
-
line.setAttribute('stroke', foreground);
|
|
159
|
-
line.setAttribute('stroke-opacity', width <= 0.5 ? '0.18' : '0.5');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
this.group.querySelectorAll('path[stroke]').forEach((path) => {
|
|
163
|
-
const original = path.dataset.graphColor || path.getAttribute('stroke');
|
|
164
|
-
if (!original || original === 'none') return;
|
|
165
|
-
path.dataset.graphColor = original;
|
|
166
|
-
path.setAttribute('stroke', ensureContrast(original, background, 3));
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
this.group.querySelectorAll('circle[fill]').forEach((circle) => {
|
|
170
|
-
const original = circle.dataset.graphColor || circle.getAttribute('fill');
|
|
171
|
-
if (!original || original === 'none') return;
|
|
172
|
-
circle.dataset.graphColor = original;
|
|
173
|
-
circle.setAttribute('fill', ensureContrast(original, background, 3));
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
this.group.querySelectorAll('text').forEach((text) => {
|
|
177
|
-
const original = text.dataset.graphColor || text.getAttribute('fill');
|
|
178
|
-
if (original) text.dataset.graphColor = original;
|
|
179
|
-
const isAxisLabel = !original || ['#8b949e', '#62627a'].includes(original.toLowerCase());
|
|
180
|
-
text.setAttribute('fill', isAxisLabel
|
|
181
|
-
? muted
|
|
182
|
-
: ensureContrast(original, background, 4.5));
|
|
183
|
-
});
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
140
|
|
|
187
141
|
window.shapes.push(graphShape);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Auto-select so the user sees something landed.
|
|
191
|
-
window.currentShape = graphShape;
|
|
192
|
-
graphShape.selectShape();
|
|
142
|
+
window.pushCreateAction?.(graphShape);
|
|
143
|
+
selectOnlyGraph(graphShape);
|
|
193
144
|
window.__adaptCanvasContrast?.(
|
|
194
|
-
window.getComputedStyle(window.svg).backgroundColor || '#
|
|
145
|
+
window.getComputedStyle(window.svg).backgroundColor || '#15111f'
|
|
195
146
|
);
|
|
196
|
-
|
|
197
|
-
|
|
147
|
+
return true;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('[GraphEngine] SVG insertion failed:', error);
|
|
198
150
|
return false;
|
|
199
151
|
}
|
|
200
|
-
|
|
201
|
-
return true;
|
|
202
152
|
}
|
|
203
153
|
|
|
204
|
-
/**
|
|
205
|
-
* Initialize the graph engine — expose bridge functions on window.
|
|
206
|
-
*/
|
|
154
|
+
/** Initialize graph bridges used by the modal and scene restoration. */
|
|
207
155
|
export function initGraphEngine() {
|
|
208
|
-
window.__graphPreview = (equations, settings) =>
|
|
209
|
-
return renderGraphPreviewSVG(equations, settings);
|
|
210
|
-
};
|
|
156
|
+
window.__graphPreview = (equations, settings) => renderGraphPreviewSVG(equations, settings);
|
|
211
157
|
window.__graphRenderer = renderGraphOnCanvas;
|
|
212
|
-
window.
|
|
213
|
-
|
|
214
|
-
return fn ? true : false;
|
|
215
|
-
};
|
|
158
|
+
window.__hydrateGraphShape = hydrateGraphShape;
|
|
159
|
+
window.__graphParser = (expression) => Boolean(parseExpression(expression));
|
|
216
160
|
window.__graphValidate = isValidExpression;
|
|
217
161
|
window.__graphColors = GRAPH_COLORS;
|
|
218
162
|
}
|
|
@@ -180,6 +180,14 @@ function serializeShape(shape) {
|
|
|
180
180
|
width: shape.width, height: shape.height,
|
|
181
181
|
rotation: shape.rotation,
|
|
182
182
|
href: el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || '',
|
|
183
|
+
sizeBytes: Number(el.__fileSize || el.getAttribute('data-file-size')) || 0,
|
|
184
|
+
cloudinaryId: el.getAttribute('data-cloudinary-id') || null,
|
|
185
|
+
storageProvider: el.getAttribute('data-storage-provider') || 'platform_cloudinary',
|
|
186
|
+
storageCloudName: el.getAttribute('data-storage-cloud-name') || null,
|
|
187
|
+
frameType: shape._frameType || null,
|
|
188
|
+
graphData: shape._frameType === 'graph' && shape._graphData
|
|
189
|
+
? cloneOptions(shape._graphData)
|
|
190
|
+
: null,
|
|
183
191
|
};
|
|
184
192
|
}
|
|
185
193
|
|
|
@@ -300,10 +308,27 @@ function deserializeShape(data) {
|
|
|
300
308
|
imgEl.setAttribute('height', data.height);
|
|
301
309
|
imgEl.setAttribute('href', data.href);
|
|
302
310
|
imgEl.setAttribute('preserveAspectRatio', 'none');
|
|
311
|
+
if (data.sizeBytes) {
|
|
312
|
+
imgEl.__fileSize = Number(data.sizeBytes) || 0;
|
|
313
|
+
imgEl.setAttribute('data-file-size', String(imgEl.__fileSize));
|
|
314
|
+
window.__roomImageBytesUsed = (window.__roomImageBytesUsed || 0) + imgEl.__fileSize;
|
|
315
|
+
}
|
|
316
|
+
if (data.cloudinaryId) imgEl.setAttribute('data-cloudinary-id', data.cloudinaryId);
|
|
317
|
+
if (data.storageProvider) imgEl.setAttribute('data-storage-provider', data.storageProvider);
|
|
318
|
+
if (data.storageCloudName) imgEl.setAttribute('data-storage-cloud-name', data.storageCloudName);
|
|
303
319
|
svgEl.appendChild(imgEl);
|
|
304
320
|
const shape = new ImageShape(imgEl);
|
|
305
321
|
if (data.rotation) shape.rotation = data.rotation;
|
|
306
322
|
if (data.shapeID) shape.shapeID = data.shapeID;
|
|
323
|
+
if (data.frameType === 'graph' && data.graphData) {
|
|
324
|
+
if (typeof window.__hydrateGraphShape === 'function') {
|
|
325
|
+
window.__hydrateGraphShape(shape, data.graphData);
|
|
326
|
+
} else {
|
|
327
|
+
shape._frameType = 'graph';
|
|
328
|
+
shape._graphData = cloneOptions(data.graphData);
|
|
329
|
+
shape.element.setAttribute('data-graph-shape', 'true');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
307
332
|
return shape;
|
|
308
333
|
}
|
|
309
334
|
|
|
@@ -387,6 +412,7 @@ export function loadScene(sceneData) {
|
|
|
387
412
|
// Clear current scene
|
|
388
413
|
const svgEl = window.svg;
|
|
389
414
|
if (!svgEl) return false;
|
|
415
|
+
window.__roomImageBytesUsed = 0;
|
|
390
416
|
|
|
391
417
|
// Remove all existing shape DOM elements
|
|
392
418
|
const existingShapes = window.shapes || [];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
const ROTATION_SELECTOR = '[data-screen-space-rotation-anchor="true"]';
|
|
4
|
+
|
|
5
|
+
function getZoom() {
|
|
6
|
+
return Math.max(0.001, Number(window.currentZoom) || 1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerRotationAnchor(anchor, options = {}) {
|
|
10
|
+
if (!anchor) return;
|
|
11
|
+
const zoom = getZoom();
|
|
12
|
+
const radius = Number(options.radius) || 8;
|
|
13
|
+
const gap = Number(options.gap) || 30;
|
|
14
|
+
const edgeY = Number(options.edgeY);
|
|
15
|
+
|
|
16
|
+
anchor.setAttribute('data-screen-space-rotation-anchor', 'true');
|
|
17
|
+
anchor.setAttribute('data-screen-radius', String(radius));
|
|
18
|
+
anchor.setAttribute('data-screen-gap', String(gap));
|
|
19
|
+
anchor.setAttribute('data-applied-zoom', String(zoom));
|
|
20
|
+
anchor.setAttribute('r', String(radius / zoom));
|
|
21
|
+
if (Number.isFinite(edgeY)) anchor.setAttribute('cy', String(edgeY - gap / zoom));
|
|
22
|
+
|
|
23
|
+
anchor.__rotationConnector = options.line || null;
|
|
24
|
+
anchor.__rotationConnectorEnd = options.lineEnd || '2';
|
|
25
|
+
syncRotationConnector(anchor);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function syncRotationConnector(anchor) {
|
|
29
|
+
const line = anchor.__rotationConnector;
|
|
30
|
+
if (!line) return;
|
|
31
|
+
const end = anchor.__rotationConnectorEnd === '1' ? '1' : '2';
|
|
32
|
+
line.setAttribute(`x${end}`, anchor.getAttribute('cx'));
|
|
33
|
+
line.setAttribute(`y${end}`, anchor.getAttribute('cy'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function syncRotationAnchorsToZoom() {
|
|
37
|
+
if (typeof document === 'undefined') return;
|
|
38
|
+
const zoom = getZoom();
|
|
39
|
+
document.querySelectorAll(ROTATION_SELECTOR).forEach((anchor) => {
|
|
40
|
+
const previousZoom = Math.max(0.001, Number(anchor.getAttribute('data-applied-zoom')) || zoom);
|
|
41
|
+
if (Math.abs(previousZoom - zoom) < 0.000001) return;
|
|
42
|
+
const radius = Number(anchor.getAttribute('data-screen-radius')) || 8;
|
|
43
|
+
const gap = Number(anchor.getAttribute('data-screen-gap')) || 30;
|
|
44
|
+
const currentY = Number(anchor.getAttribute('cy')) || 0;
|
|
45
|
+
const edgeY = currentY + gap / previousZoom;
|
|
46
|
+
|
|
47
|
+
anchor.setAttribute('cy', String(edgeY - gap / zoom));
|
|
48
|
+
anchor.setAttribute('r', String(radius / zoom));
|
|
49
|
+
anchor.setAttribute('data-applied-zoom', String(zoom));
|
|
50
|
+
syncRotationConnector(anchor);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function installScreenSpaceControlSync(svgElement) {
|
|
55
|
+
if (!svgElement || typeof MutationObserver === 'undefined') return;
|
|
56
|
+
window.__screenSpaceControlObserver?.disconnect?.();
|
|
57
|
+
const observer = new MutationObserver(syncRotationAnchorsToZoom);
|
|
58
|
+
observer.observe(svgElement, { attributes: true, attributeFilter: ['viewBox'] });
|
|
59
|
+
window.__screenSpaceControlObserver = observer;
|
|
60
|
+
}
|