@elixpo/lixsketch 5.6.5 → 5.6.6
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/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +2 -2
- package/dist/mcp/stdio.js +5 -1
- package/dist/mcp/stdio.js.map +2 -2
- package/dist/react/{AIRenderer-SRF7LU5K.js → AIRenderer-R5M2CIDO.js} +13 -6
- package/dist/react/AIRenderer-R5M2CIDO.js.map +7 -0
- package/dist/react/Circle-HMM5PKJ2.js +10 -0
- package/dist/react/{CopyPaste-YUDZ6DHY.js → CopyPaste-P6G35CXO.js} +11 -10
- package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-P6G35CXO.js.map} +1 -1
- package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-M5B4EWHJ.js} +216 -63
- package/dist/react/EventDispatcher-M5B4EWHJ.js.map +7 -0
- package/dist/react/Frame-QEUEYITT.js +11 -0
- package/dist/react/{FreehandStroke-U6GYZOM4.js → FreehandStroke-WXIJKD5I.js} +2 -2
- package/dist/react/{GraphEngine-MM6ALA2N.js → GraphEngine-XH3RPYO4.js} +9 -2
- package/dist/react/GraphEngine-XH3RPYO4.js.map +7 -0
- package/dist/react/IconShape-NHJE4YBY.js +10 -0
- package/dist/react/ImageShape-33VCMSDP.js +10 -0
- package/dist/react/Line-A6FE232Q.js +10 -0
- package/dist/react/{LixScriptParser-JYWJX375.js → LixScriptParser-YS3ZOTGS.js} +6 -2
- package/dist/react/LixScriptParser-YS3ZOTGS.js.map +7 -0
- package/dist/react/{MermaidFlowchartRenderer-PQEC4TMI.js → MermaidFlowchartRenderer-EL3Q2MNW.js} +6 -2
- package/dist/react/MermaidFlowchartRenderer-EL3Q2MNW.js.map +7 -0
- package/dist/react/MermaidSequenceParser-DBYXW23P.js +8 -0
- package/dist/react/{MermaidSequenceRenderer-KCUHO7UI.js → MermaidSequenceRenderer-K4KHHEVC.js} +10 -3
- package/dist/react/MermaidSequenceRenderer-K4KHHEVC.js.map +7 -0
- package/dist/react/{MermaidStructuredRenderer-W7UKN7BM.js → MermaidStructuredRenderer-6LN3WISP.js} +7 -2
- package/dist/react/MermaidStructuredRenderer-6LN3WISP.js.map +7 -0
- package/dist/react/Rectangle-4U4CD2ER.js +10 -0
- package/dist/react/{SceneSerializer-SG6SJN5X.js → SceneSerializer-JEYTYXZQ.js} +120 -46
- package/dist/react/SceneSerializer-JEYTYXZQ.js.map +7 -0
- package/dist/react/{Selection-2TSH3J6P.js → Selection-23MOLHEI.js} +4 -4
- package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-OPJON7TD.js} +26 -26
- package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-QVNRI5OB.js} +8 -2
- package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-VH3PJPDM.js} +3 -3
- package/dist/react/{chunk-LSHZYN77.js → chunk-2DZHO67H.js} +2 -2
- package/dist/react/{chunk-XOWNCYE4.js → chunk-5NHJJJBZ.js} +3 -3
- package/dist/react/chunk-6AIFXYV6.js +31 -0
- package/dist/react/chunk-6AIFXYV6.js.map +7 -0
- package/dist/react/{chunk-K5RY4EV7.js → chunk-6GPWSPJF.js} +9 -5
- package/dist/react/{chunk-K5RY4EV7.js.map → chunk-6GPWSPJF.js.map} +2 -2
- package/dist/react/{chunk-ASBC5G35.js → chunk-B6CYUZC4.js} +2 -2
- package/dist/react/{chunk-OP56XPWF.js → chunk-DNFPTVIP.js} +2 -2
- package/dist/react/{chunk-XYUFEIZR.js → chunk-DWEFPKDO.js} +2 -2
- package/dist/react/{chunk-GQUZPON4.js → chunk-F56XUE5S.js} +4 -4
- package/dist/react/{chunk-7CM5XQDN.js → chunk-G3DJQY65.js} +20 -36
- package/dist/react/chunk-G3DJQY65.js.map +7 -0
- package/dist/react/{chunk-LJR7EVW2.js → chunk-JEGLUNBC.js} +3 -3
- package/dist/react/{chunk-6GPEWDRB.js → chunk-M7GAB24F.js} +52 -6
- package/dist/react/chunk-M7GAB24F.js.map +7 -0
- package/dist/react/{chunk-VQVEPJSA.js → chunk-MHV5P6TS.js} +2 -1
- package/dist/react/chunk-MHV5P6TS.js.map +7 -0
- package/dist/react/{chunk-PXFPPO3R.js → chunk-NH3NVSWT.js} +18 -2
- package/dist/react/chunk-NH3NVSWT.js.map +7 -0
- package/dist/react/{chunk-XFL4R5ZF.js → chunk-OK4ROGMM.js} +16 -35
- package/dist/react/chunk-OK4ROGMM.js.map +7 -0
- package/dist/react/{chunk-APTOPAOY.js → chunk-SSFEN5CY.js} +3 -3
- package/dist/react/{chunk-RGMDWLRJ.js → chunk-UEL2G6D2.js} +3 -3
- package/dist/react/{chunk-M6TMN76K.js → chunk-ULPIHUVC.js} +3 -3
- package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
- package/dist/react/chunk-VZPH7OAM.js.map +7 -0
- package/dist/react/{chunk-TKXKJNTX.js → chunk-W7XUY4GR.js} +16 -19
- package/dist/react/chunk-W7XUY4GR.js.map +7 -0
- package/dist/react/{chunk-EOURRJ55.js → chunk-XSE4HQ2D.js} +2 -2
- package/dist/react/{chunk-K5BG5MHY.js → chunk-ZNZAICWY.js} +36 -60
- package/dist/react/chunk-ZNZAICWY.js.map +7 -0
- package/dist/react/{circleTool-LDKGZALF.js → circleTool-2UODOBCV.js} +4 -4
- package/dist/react/{codeTool-4YZO5W7J.js → codeTool-F4RV7SAI.js} +4 -4
- package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-NTGA5BI6.js} +4 -4
- package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-L55EUB5C.js} +4 -4
- package/dist/react/{iconTool-46O5VEUX.js → iconTool-KQQBLGR3.js} +4 -4
- package/dist/react/{imageTool-P2RWZRHO.js → imageTool-LDJ45AD7.js} +4 -4
- package/dist/react/index.js +148 -37
- package/dist/react/index.js.map +4 -4
- package/dist/react/{lineTool-ZMR27U5L.js → lineTool-CSAFD7PW.js} +4 -4
- package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-YPD2SIIZ.js} +4 -4
- package/dist/react/{textTool-S4XXZHZH.js → textTool-VQHMWEFJ.js} +5 -5
- package/package.json +5 -2
- package/src/core/AIRenderer.js +8 -2
- package/src/core/CanvasSpace.js +28 -0
- package/src/core/GraphRenderer.js +8 -3
- package/src/core/LixScriptParser.js +7 -1
- package/src/core/MermaidFlowchartRenderer.js +5 -1
- package/src/core/MermaidSequenceParser.js +1 -0
- package/src/core/MermaidSequenceRenderer.js +8 -3
- package/src/core/MermaidStructuredRenderer.js +6 -1
- package/src/core/SceneSerializer.js +121 -38
- package/src/core/Selection.js +12 -2
- package/src/core/UndoRedo.js +59 -5
- package/src/react/components/AppMenu.jsx +20 -19
- package/src/react/components/canvas/CanvasRulers.jsx +8 -13
- package/src/react/components/canvas/SVGCanvas.jsx +2 -1
- package/src/react/components/canvas/rulerUnits.js +28 -0
- package/src/react/components/sidebars/PaintBucketSidebar.jsx +1 -1
- package/src/react/store/useSketchStore.js +24 -3
- package/src/react/store/useUIStore.js +24 -1
- package/src/react/utils/canvasBackgrounds.js +29 -0
- package/src/shapes/Circle.js +12 -19
- package/src/shapes/Frame.js +38 -69
- package/src/shapes/FreehandStroke.js +24 -1
- package/src/shapes/Rectangle.js +10 -22
- package/src/tools/circleTool.js +3 -27
- package/src/tools/paintBucketTool.js +72 -15
- package/src/tools/rectangleTool.js +7 -23
- package/src/tools/shapeRecognition.js +270 -0
- package/src/tools/shapeRecognitionTool.js +36 -183
- package/dist/react/AIRenderer-SRF7LU5K.js.map +0 -7
- package/dist/react/Circle-6POZCUZD.js +0 -9
- package/dist/react/EventDispatcher-7LAK7GQ7.js.map +0 -7
- package/dist/react/Frame-7D2MXCFF.js +0 -11
- package/dist/react/GraphEngine-MM6ALA2N.js.map +0 -7
- package/dist/react/IconShape-ZADX7Y33.js +0 -10
- package/dist/react/ImageShape-UJQN2HI7.js +0 -10
- package/dist/react/Line-APEULMHE.js +0 -10
- package/dist/react/LixScriptParser-JYWJX375.js.map +0 -7
- package/dist/react/MermaidFlowchartRenderer-PQEC4TMI.js.map +0 -7
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js +0 -8
- package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +0 -7
- package/dist/react/MermaidStructuredRenderer-W7UKN7BM.js.map +0 -7
- package/dist/react/Rectangle-S7XUKWYD.js +0 -9
- package/dist/react/SceneSerializer-SG6SJN5X.js.map +0 -7
- package/dist/react/chunk-6GPEWDRB.js.map +0 -7
- package/dist/react/chunk-7CM5XQDN.js.map +0 -7
- package/dist/react/chunk-FETTJLCV.js.map +0 -7
- package/dist/react/chunk-K5BG5MHY.js.map +0 -7
- package/dist/react/chunk-PXFPPO3R.js.map +0 -7
- package/dist/react/chunk-TKXKJNTX.js.map +0 -7
- package/dist/react/chunk-VQVEPJSA.js.map +0 -7
- package/dist/react/chunk-XFL4R5ZF.js.map +0 -7
- /package/dist/react/{Circle-6POZCUZD.js.map → Circle-HMM5PKJ2.js.map} +0 -0
- /package/dist/react/{Frame-7D2MXCFF.js.map → Frame-QEUEYITT.js.map} +0 -0
- /package/dist/react/{FreehandStroke-U6GYZOM4.js.map → FreehandStroke-WXIJKD5I.js.map} +0 -0
- /package/dist/react/{IconShape-ZADX7Y33.js.map → IconShape-NHJE4YBY.js.map} +0 -0
- /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-33VCMSDP.js.map} +0 -0
- /package/dist/react/{Line-APEULMHE.js.map → Line-A6FE232Q.js.map} +0 -0
- /package/dist/react/{MermaidSequenceParser-UKG2NSZY.js.map → MermaidSequenceParser-DBYXW23P.js.map} +0 -0
- /package/dist/react/{Rectangle-S7XUKWYD.js.map → Rectangle-4U4CD2ER.js.map} +0 -0
- /package/dist/react/{Selection-2TSH3J6P.js.map → Selection-23MOLHEI.js.map} +0 -0
- /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-OPJON7TD.js.map} +0 -0
- /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-QVNRI5OB.js.map} +0 -0
- /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-VH3PJPDM.js.map} +0 -0
- /package/dist/react/{chunk-LSHZYN77.js.map → chunk-2DZHO67H.js.map} +0 -0
- /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-5NHJJJBZ.js.map} +0 -0
- /package/dist/react/{chunk-ASBC5G35.js.map → chunk-B6CYUZC4.js.map} +0 -0
- /package/dist/react/{chunk-OP56XPWF.js.map → chunk-DNFPTVIP.js.map} +0 -0
- /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-DWEFPKDO.js.map} +0 -0
- /package/dist/react/{chunk-GQUZPON4.js.map → chunk-F56XUE5S.js.map} +0 -0
- /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-JEGLUNBC.js.map} +0 -0
- /package/dist/react/{chunk-APTOPAOY.js.map → chunk-SSFEN5CY.js.map} +0 -0
- /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-UEL2G6D2.js.map} +0 -0
- /package/dist/react/{chunk-M6TMN76K.js.map → chunk-ULPIHUVC.js.map} +0 -0
- /package/dist/react/{chunk-EOURRJ55.js.map → chunk-XSE4HQ2D.js.map} +0 -0
- /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-2UODOBCV.js.map} +0 -0
- /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-F4RV7SAI.js.map} +0 -0
- /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-NTGA5BI6.js.map} +0 -0
- /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-L55EUB5C.js.map} +0 -0
- /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-KQQBLGR3.js.map} +0 -0
- /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-LDJ45AD7.js.map} +0 -0
- /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-CSAFD7PW.js.map} +0 -0
- /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-YPD2SIIZ.js.map} +0 -0
- /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-VQHMWEFJ.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-UEL2G6D2.js";
|
|
7
|
+
import "./chunk-DWEFPKDO.js";
|
|
8
|
+
import "./chunk-M7GAB24F.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-CSAFD7PW.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-G3DJQY65.js";
|
|
7
|
+
import "./chunk-DWEFPKDO.js";
|
|
8
|
+
import "./chunk-M7GAB24F.js";
|
|
9
9
|
export {
|
|
10
10
|
handleMouseDownRect,
|
|
11
11
|
handleMouseMoveRect,
|
|
12
12
|
handleMouseUpRect
|
|
13
13
|
};
|
|
14
|
-
//# sourceMappingURL=rectangleTool-
|
|
14
|
+
//# sourceMappingURL=rectangleTool-YPD2SIIZ.js.map
|
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
handleTextMouseMove,
|
|
7
7
|
handleTextMouseUp,
|
|
8
8
|
updateCodeToggleForShape
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-F56XUE5S.js";
|
|
10
|
+
import "./chunk-ULPIHUVC.js";
|
|
11
11
|
import "./chunk-PAKA6HUX.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-DWEFPKDO.js";
|
|
13
|
+
import "./chunk-M7GAB24F.js";
|
|
14
14
|
export {
|
|
15
15
|
deselectElement as deselectTextElement,
|
|
16
16
|
enterEditMode,
|
|
@@ -19,4 +19,4 @@ export {
|
|
|
19
19
|
handleTextMouseUp,
|
|
20
20
|
updateCodeToggleForShape
|
|
21
21
|
};
|
|
22
|
-
//# sourceMappingURL=textTool-
|
|
22
|
+
//# sourceMappingURL=textTool-VQHMWEFJ.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elixpo/lixsketch",
|
|
3
|
-
"version": "5.6.
|
|
3
|
+
"version": "5.6.6",
|
|
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",
|
|
@@ -82,10 +82,13 @@
|
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"esbuild": "^0.24.0"
|
|
85
|
+
"esbuild": "^0.24.0",
|
|
86
|
+
"vitest": "^3.2.4"
|
|
86
87
|
},
|
|
87
88
|
"scripts": {
|
|
88
89
|
"build": "node build.mjs",
|
|
90
|
+
"test": "npm run test:unit && npm run test:mcp",
|
|
91
|
+
"test:unit": "vitest run",
|
|
89
92
|
"test:mcp": "node --test test/mcp-*.test.js",
|
|
90
93
|
"prepublishOnly": "npm run build"
|
|
91
94
|
}
|
package/src/core/AIRenderer.js
CHANGED
|
@@ -44,8 +44,14 @@ export function parseMermaid(src) {
|
|
|
44
44
|
const classDefs = new Map(); // classDef name -> { fill, stroke, strokeWidth }
|
|
45
45
|
const classAssigns = []; // { nodeIds: [...], className }
|
|
46
46
|
const nodeStyles = new Map(); // Mermaid `style A fill:...,stroke:...`
|
|
47
|
-
const darkTheme =
|
|
48
|
-
|
|
47
|
+
const darkTheme = (() => {
|
|
48
|
+
if (typeof document === 'undefined') return true;
|
|
49
|
+
if (document.body) {
|
|
50
|
+
if (document.body.classList.contains('theme-dark')) return true;
|
|
51
|
+
if (document.body.classList.contains('theme-light')) return false;
|
|
52
|
+
}
|
|
53
|
+
return !document.documentElement || document.documentElement.classList.contains('dark');
|
|
54
|
+
})();
|
|
49
55
|
const palette = darkTheme
|
|
50
56
|
? [
|
|
51
57
|
{ fill: '#352b52', stroke: '#9d86df' },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// Tool pointer coordinates are already in SVG viewBox space. getCTM() also
|
|
4
|
+
// includes viewport pan/zoom, so controls use these helpers for shape-only
|
|
5
|
+
// translation and rotation.
|
|
6
|
+
export function localToCanvas(point, transform) {
|
|
7
|
+
const rotation = (Number(transform.rotation) || 0) * Math.PI / 180;
|
|
8
|
+
const centerX = Number(transform.centerX) || 0;
|
|
9
|
+
const centerY = Number(transform.centerY) || 0;
|
|
10
|
+
const dx = point.x - centerX;
|
|
11
|
+
const dy = point.y - centerY;
|
|
12
|
+
return {
|
|
13
|
+
x: (Number(transform.x) || 0) + centerX + dx * Math.cos(rotation) - dy * Math.sin(rotation),
|
|
14
|
+
y: (Number(transform.y) || 0) + centerY + dx * Math.sin(rotation) + dy * Math.cos(rotation),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function canvasToLocal(point, transform) {
|
|
19
|
+
const rotation = -(Number(transform.rotation) || 0) * Math.PI / 180;
|
|
20
|
+
const centerX = Number(transform.centerX) || 0;
|
|
21
|
+
const centerY = Number(transform.centerY) || 0;
|
|
22
|
+
const dx = point.x - (Number(transform.x) || 0) - centerX;
|
|
23
|
+
const dy = point.y - (Number(transform.y) || 0) - centerY;
|
|
24
|
+
return {
|
|
25
|
+
x: centerX + dx * Math.cos(rotation) - dy * Math.sin(rotation),
|
|
26
|
+
y: centerY + dx * Math.sin(rotation) + dy * Math.cos(rotation),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -17,9 +17,14 @@ const GRAPH_COLORS = [
|
|
|
17
17
|
// labels stay readable on the warm off-white canvas. The equation
|
|
18
18
|
// curves themselves keep their colours from GRAPH_COLORS.
|
|
19
19
|
function graphThemeTokens() {
|
|
20
|
-
const isDark =
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const isDark = (() => {
|
|
21
|
+
if (typeof document === 'undefined') return true;
|
|
22
|
+
if (document.body) {
|
|
23
|
+
if (document.body.classList.contains('theme-dark')) return true;
|
|
24
|
+
if (document.body.classList.contains('theme-light')) return false;
|
|
25
|
+
}
|
|
26
|
+
return !document.documentElement || document.documentElement.classList.contains('dark');
|
|
27
|
+
})();
|
|
23
28
|
if (isDark) {
|
|
24
29
|
return {
|
|
25
30
|
outerBg: '#0d1117',
|
|
@@ -50,11 +50,17 @@ function tokenize(source) {
|
|
|
50
50
|
* Returns { variables, shapes, errors }
|
|
51
51
|
*/
|
|
52
52
|
export function parseLixScript(source) {
|
|
53
|
-
const tokens = tokenize(source)
|
|
54
53
|
const variables = {}
|
|
55
54
|
const shapes = []
|
|
56
55
|
const errors = []
|
|
57
56
|
|
|
57
|
+
if (typeof source !== 'string') {
|
|
58
|
+
errors.push({ line: 0, message: 'LixScript source must be a string' })
|
|
59
|
+
return { variables, shapes, errors }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tokens = tokenize(source)
|
|
63
|
+
|
|
58
64
|
let i = 0
|
|
59
65
|
|
|
60
66
|
while (i < tokens.length) {
|
|
@@ -22,7 +22,11 @@ const FONT_FAMILY = 'lixFont, sans-serif';
|
|
|
22
22
|
// so a single render call uses whichever palette is active.
|
|
23
23
|
function isThemeDark() {
|
|
24
24
|
if (typeof document === 'undefined') return true;
|
|
25
|
-
|
|
25
|
+
if (document.body) {
|
|
26
|
+
if (document.body.classList.contains('theme-dark')) return true;
|
|
27
|
+
if (document.body.classList.contains('theme-light')) return false;
|
|
28
|
+
}
|
|
29
|
+
return !document.documentElement || document.documentElement.classList.contains('dark');
|
|
26
30
|
}
|
|
27
31
|
function nodeStrokeColor() { return isThemeDark() ? '#fff' : '#1a1a2e'; }
|
|
28
32
|
function edgeStrokeColor() { return isThemeDark() ? '#888' : '#444'; }
|
|
@@ -28,6 +28,7 @@ const ARROW_TYPES = {
|
|
|
28
28
|
const ARROW_PATTERNS = Object.keys(ARROW_TYPES).sort((a, b) => b.length - a.length);
|
|
29
29
|
|
|
30
30
|
export function parseSequenceDiagram(src) {
|
|
31
|
+
if (typeof src !== 'string') return null;
|
|
31
32
|
const lines = src.trim().split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('%%'));
|
|
32
33
|
if (lines.length === 0) return null;
|
|
33
34
|
|
|
@@ -32,9 +32,14 @@ const CODE_FONT = 'lixCode, monospace';
|
|
|
32
32
|
// string preview path (`renderSequenceSVG`), which is rendered inside
|
|
33
33
|
// the modal's preview pane.
|
|
34
34
|
function themeColors() {
|
|
35
|
-
const isDark =
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const isDark = (() => {
|
|
36
|
+
if (typeof document === 'undefined') return true;
|
|
37
|
+
if (document.body) {
|
|
38
|
+
if (document.body.classList.contains('theme-dark')) return true;
|
|
39
|
+
if (document.body.classList.contains('theme-light')) return false;
|
|
40
|
+
}
|
|
41
|
+
return !document.documentElement || document.documentElement.classList.contains('dark');
|
|
42
|
+
})();
|
|
38
43
|
if (isDark) return DARK_THEME;
|
|
39
44
|
return {
|
|
40
45
|
bg: '#fbfaf6',
|
|
@@ -37,7 +37,12 @@ function contrastText(fill) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function isDark() {
|
|
40
|
-
|
|
40
|
+
if (typeof document === 'undefined') return true;
|
|
41
|
+
if (document.body) {
|
|
42
|
+
if (document.body.classList.contains('theme-dark')) return true;
|
|
43
|
+
if (document.body.classList.contains('theme-light')) return false;
|
|
44
|
+
}
|
|
45
|
+
return !document.documentElement || document.documentElement.classList.contains('dark');
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
function theme() {
|
|
@@ -17,8 +17,14 @@ import { TextShape } from '../shapes/TextShape.js';
|
|
|
17
17
|
import { CodeShape } from '../shapes/CodeShape.js';
|
|
18
18
|
import { ImageShape } from '../shapes/ImageShape.js';
|
|
19
19
|
import { IconShape } from '../shapes/IconShape.js';
|
|
20
|
+
import { clearUndoHistory, pushCanvasResetAction } from './UndoRedo.js';
|
|
20
21
|
|
|
21
22
|
const FORMAT_VERSION = 1;
|
|
23
|
+
const GZIP_MAGIC = [0x1f, 0x8b];
|
|
24
|
+
const SUPPORTED_SHAPE_TYPES = new Set([
|
|
25
|
+
'rectangle', 'circle', 'line', 'arrow', 'freehandStroke',
|
|
26
|
+
'frame', 'text', 'code', 'image', 'icon',
|
|
27
|
+
]);
|
|
22
28
|
|
|
23
29
|
// Generate a unique session ID for each scene
|
|
24
30
|
let _sessionID = null;
|
|
@@ -413,8 +419,9 @@ export function saveScene(workspaceName = 'Untitled') {
|
|
|
413
419
|
// LOAD: Deserialize .lixsketch JSON and recreate scene
|
|
414
420
|
// ============================================================
|
|
415
421
|
export function loadScene(sceneData) {
|
|
416
|
-
|
|
417
|
-
|
|
422
|
+
const validation = validateScene(sceneData);
|
|
423
|
+
if (!validation.valid) {
|
|
424
|
+
console.error('[SceneSerializer] Invalid scene:', validation.error);
|
|
418
425
|
return false;
|
|
419
426
|
}
|
|
420
427
|
|
|
@@ -525,6 +532,20 @@ export function loadScene(sceneData) {
|
|
|
525
532
|
}
|
|
526
533
|
}
|
|
527
534
|
|
|
535
|
+
// Shape-array order is the scene's z-order. Frames are constructed first
|
|
536
|
+
// so their clip groups exist before children are attached, but that is an
|
|
537
|
+
// implementation detail and must not change the saved stacking order.
|
|
538
|
+
const restoredInSceneOrder = sceneData.shapes
|
|
539
|
+
.map(data => data.shapeID && idMap.get(data.shapeID))
|
|
540
|
+
.filter(Boolean);
|
|
541
|
+
const restoredSet = new Set(restoredInSceneOrder);
|
|
542
|
+
window.shapes.splice(
|
|
543
|
+
0,
|
|
544
|
+
window.shapes.length,
|
|
545
|
+
...restoredInSceneOrder,
|
|
546
|
+
...window.shapes.filter(shape => !restoredSet.has(shape)),
|
|
547
|
+
);
|
|
548
|
+
|
|
528
549
|
// ── Fourth pass: frame containment reconciliation (issue #24 bug #10) ──
|
|
529
550
|
//
|
|
530
551
|
// The pass above attaches each child via `frame.addShapeToFrame(child)`,
|
|
@@ -608,21 +629,62 @@ export function loadScene(sceneData) {
|
|
|
608
629
|
}
|
|
609
630
|
|
|
610
631
|
// ============================================================
|
|
611
|
-
// DOWNLOAD: Trigger browser download of .
|
|
632
|
+
// DOWNLOAD: Trigger browser download of an optimized .lixjson file
|
|
612
633
|
// ============================================================
|
|
613
|
-
|
|
634
|
+
function sceneFileTimestamp(date = new Date()) {
|
|
635
|
+
const iso = date.toISOString();
|
|
636
|
+
return `${iso.slice(0, 10).replace(/-/g, '')}-${iso.slice(11, 19).replace(/:/g, '')}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function safeWorkspaceFileName(workspaceName) {
|
|
640
|
+
const safe = String(workspaceName || 'Untitled')
|
|
641
|
+
.trim()
|
|
642
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
643
|
+
.replace(/^_+|_+$/g, '');
|
|
644
|
+
return safe || 'Untitled';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export async function encodeSceneFile(scene) {
|
|
648
|
+
const json = JSON.stringify(scene);
|
|
649
|
+
if (typeof CompressionStream === 'undefined') {
|
|
650
|
+
return new Blob([json], { type: 'application/json' });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const compressed = new Blob([json])
|
|
654
|
+
.stream()
|
|
655
|
+
.pipeThrough(new CompressionStream('gzip'));
|
|
656
|
+
const buffer = await new Response(compressed).arrayBuffer();
|
|
657
|
+
return new Blob([buffer], { type: 'application/vnd.lixsketch.scene+gzip' });
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export async function decodeSceneFile(buffer) {
|
|
661
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
662
|
+
const isGzip = bytes[0] === GZIP_MAGIC[0] && bytes[1] === GZIP_MAGIC[1];
|
|
663
|
+
if (!isGzip) return new TextDecoder().decode(bytes);
|
|
664
|
+
if (typeof DecompressionStream === 'undefined') {
|
|
665
|
+
throw new Error('This browser cannot open compressed .lixjson files.');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const decompressed = new Blob([bytes])
|
|
669
|
+
.stream()
|
|
670
|
+
.pipeThrough(new DecompressionStream('gzip'));
|
|
671
|
+
return new Response(decompressed).text();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function downloadScene(workspaceName = 'Untitled') {
|
|
614
675
|
const scene = saveScene(workspaceName);
|
|
615
|
-
const
|
|
616
|
-
const blob = new Blob([json], { type: 'application/json' });
|
|
676
|
+
const blob = await encodeSceneFile(scene);
|
|
617
677
|
const url = URL.createObjectURL(blob);
|
|
678
|
+
const fileName = `${safeWorkspaceFileName(workspaceName)}-${sceneFileTimestamp()}.lixjson`;
|
|
618
679
|
|
|
619
680
|
const a = document.createElement('a');
|
|
620
681
|
a.href = url;
|
|
621
|
-
a.download =
|
|
682
|
+
a.download = fileName;
|
|
622
683
|
document.body.appendChild(a);
|
|
623
684
|
a.click();
|
|
624
685
|
document.body.removeChild(a);
|
|
625
|
-
URL.revokeObjectURL(url);
|
|
686
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
687
|
+
return { fileName, sizeBytes: blob.size, compressed: blob.type.includes('gzip') };
|
|
626
688
|
}
|
|
627
689
|
|
|
628
690
|
// ============================================================
|
|
@@ -634,6 +696,15 @@ export function validateScene(data) {
|
|
|
634
696
|
if (data.format !== 'lixsketch') return { valid: false, error: 'Not a LixSketch scene file (missing format field)' };
|
|
635
697
|
if (!data.version || data.version > FORMAT_VERSION) return { valid: false, error: `Unsupported version: ${data.version}` };
|
|
636
698
|
if (!Array.isArray(data.shapes)) return { valid: false, error: 'Invalid scene: missing shapes array' };
|
|
699
|
+
for (let index = 0; index < data.shapes.length; index += 1) {
|
|
700
|
+
const shape = data.shapes[index];
|
|
701
|
+
if (!shape || typeof shape !== 'object' || Array.isArray(shape)) {
|
|
702
|
+
return { valid: false, error: `Invalid shape at index ${index}` };
|
|
703
|
+
}
|
|
704
|
+
if (!SUPPORTED_SHAPE_TYPES.has(shape.type)) {
|
|
705
|
+
return { valid: false, error: `Unsupported shape type at index ${index}: ${String(shape.type)}` };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
637
708
|
return {
|
|
638
709
|
valid: true,
|
|
639
710
|
name: data.name || 'Untitled',
|
|
@@ -648,29 +719,26 @@ export function uploadScene() {
|
|
|
648
719
|
const input = document.createElement('input');
|
|
649
720
|
input.type = 'file';
|
|
650
721
|
input.accept = '.lixjson,.json';
|
|
651
|
-
input.onchange = (e) => {
|
|
722
|
+
input.onchange = async (e) => {
|
|
652
723
|
const file = e.target.files[0];
|
|
653
724
|
if (!file) return resolve(false);
|
|
654
725
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
resetSessionID(); // New session for loaded scene
|
|
666
|
-
const result = loadScene(data);
|
|
667
|
-
resolve({ success: result, validation });
|
|
668
|
-
} catch (err) {
|
|
669
|
-
console.error('[SceneSerializer] Failed to parse file:', err);
|
|
670
|
-
resolve({ success: false, error: 'Failed to parse JSON file' });
|
|
726
|
+
try {
|
|
727
|
+
const json = await decodeSceneFile(await file.arrayBuffer());
|
|
728
|
+
const data = JSON.parse(json);
|
|
729
|
+
const validation = validateScene(data);
|
|
730
|
+
if (!validation.valid) {
|
|
731
|
+
console.error('[SceneSerializer] Invalid file:', validation.error);
|
|
732
|
+
resolve({ success: false, error: validation.error });
|
|
733
|
+
return;
|
|
671
734
|
}
|
|
672
|
-
|
|
673
|
-
|
|
735
|
+
resetSessionID(); // New session for loaded scene
|
|
736
|
+
const result = loadScene(data);
|
|
737
|
+
resolve({ success: result, validation });
|
|
738
|
+
} catch (err) {
|
|
739
|
+
console.error('[SceneSerializer] Failed to parse file:', err);
|
|
740
|
+
resolve({ success: false, error: err.message || 'Failed to parse .lixjson file' });
|
|
741
|
+
}
|
|
674
742
|
};
|
|
675
743
|
input.click();
|
|
676
744
|
});
|
|
@@ -808,12 +876,15 @@ export function exportAsPDF() {
|
|
|
808
876
|
// ============================================================
|
|
809
877
|
// RESET: Clear the entire canvas
|
|
810
878
|
// ============================================================
|
|
811
|
-
export function resetCanvas() {
|
|
879
|
+
export function resetCanvas({ undoable = true, clearStorage = true, clearHistory = true, workspaceName = 'Untitled' } = {}) {
|
|
812
880
|
const svgEl = window.svg;
|
|
813
881
|
if (!svgEl) return;
|
|
814
882
|
|
|
815
883
|
// Remove all shape DOM elements + frame clipGroups/clipPaths
|
|
816
884
|
const existingShapes = window.shapes || [];
|
|
885
|
+
const resetSnapshot = undoable && existingShapes.length > 0
|
|
886
|
+
? saveScene(workspaceName)
|
|
887
|
+
: null;
|
|
817
888
|
existingShapes.forEach(shape => {
|
|
818
889
|
if (shape.shapeName === 'frame') {
|
|
819
890
|
if (shape.clipGroup && shape.clipGroup.parentNode) {
|
|
@@ -848,16 +919,28 @@ export function resetCanvas() {
|
|
|
848
919
|
window.disableAllSideBars();
|
|
849
920
|
}
|
|
850
921
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
922
|
+
if (clearStorage) {
|
|
923
|
+
// Clear auto-save (both legacy and session-scoped keys)
|
|
924
|
+
try {
|
|
925
|
+
localStorage.removeItem('lixsketch-autosave');
|
|
926
|
+
localStorage.removeItem('lixsketch-autosave-meta');
|
|
927
|
+
const sid = window.__sessionID;
|
|
928
|
+
if (sid) {
|
|
929
|
+
localStorage.removeItem(`lixsketch-autosave-${sid}`);
|
|
930
|
+
localStorage.removeItem(`lixsketch-autosave-meta-${sid}`);
|
|
931
|
+
}
|
|
932
|
+
} catch (_) {}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (resetSnapshot) {
|
|
936
|
+
pushCanvasResetAction(
|
|
937
|
+
resetSnapshot,
|
|
938
|
+
(snapshot) => loadScene(snapshot),
|
|
939
|
+
() => resetCanvas({ undoable: false, clearStorage: true, clearHistory: false, workspaceName }),
|
|
940
|
+
);
|
|
941
|
+
} else if (clearHistory) {
|
|
942
|
+
clearUndoHistory();
|
|
943
|
+
}
|
|
861
944
|
|
|
862
945
|
console.log('[SceneSerializer] Canvas reset');
|
|
863
946
|
if (typeof window.__collabSceneChanged === 'function') {
|
package/src/core/Selection.js
CHANGED
|
@@ -1864,6 +1864,16 @@ document.addEventListener('keydown', (e) => {
|
|
|
1864
1864
|
function deleteSelectedShapes() {
|
|
1865
1865
|
if (multiSelection.selectedShapes.size === 0) return;
|
|
1866
1866
|
|
|
1867
|
+
if (
|
|
1868
|
+
Array.isArray(window.shapes) &&
|
|
1869
|
+
window.shapes.length > 0 &&
|
|
1870
|
+
multiSelection.selectedShapes.size >= window.shapes.length &&
|
|
1871
|
+
typeof window.__requestCanvasResetConfirmation === 'function'
|
|
1872
|
+
) {
|
|
1873
|
+
window.__requestCanvasResetConfirmation();
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1867
1877
|
const shapesToDelete = Array.from(multiSelection.selectedShapes);
|
|
1868
1878
|
multiSelection.clearSelection();
|
|
1869
1879
|
|
|
@@ -1938,9 +1948,9 @@ function deleteSelectedShapes() {
|
|
|
1938
1948
|
// snapshot so undo can re-attach them.
|
|
1939
1949
|
try {
|
|
1940
1950
|
if (childSnapshot) {
|
|
1941
|
-
pushDeleteAction(shape, { childSnapshot });
|
|
1951
|
+
pushDeleteAction(shape, { childSnapshot, shapeIndex: idx });
|
|
1942
1952
|
} else {
|
|
1943
|
-
pushDeleteAction(shape);
|
|
1953
|
+
pushDeleteAction(shape, { shapeIndex: idx });
|
|
1944
1954
|
}
|
|
1945
1955
|
} catch (err) {
|
|
1946
1956
|
console.warn('[deleteSelectedShapes] pushDeleteAction failed:', err);
|
package/src/core/UndoRedo.js
CHANGED
|
@@ -58,6 +58,7 @@ export function pushDeleteAction(shape, meta = null) {
|
|
|
58
58
|
// when the frame is restored, instead of leaving an empty box.
|
|
59
59
|
const action = { type: 'delete', shape };
|
|
60
60
|
if (meta && meta.childSnapshot) action.childSnapshot = meta.childSnapshot;
|
|
61
|
+
if (Number.isInteger(meta?.shapeIndex)) action.shapeIndex = meta.shapeIndex;
|
|
61
62
|
undoStack.push(action);
|
|
62
63
|
|
|
63
64
|
// Clear redo stack when new action is performed
|
|
@@ -65,6 +66,30 @@ export function pushDeleteAction(shape, meta = null) {
|
|
|
65
66
|
notifyCollaboration();
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
export function pushCanvasResetAction(snapshot, restore, reset) {
|
|
70
|
+
if (!snapshot || typeof restore !== 'function' || typeof reset !== 'function') return;
|
|
71
|
+
// A reset is a new history boundary. Older per-shape actions reference
|
|
72
|
+
// objects that the scene loader will replace, so only the atomic reset
|
|
73
|
+
// snapshot remains undoable.
|
|
74
|
+
undoStack.length = 0;
|
|
75
|
+
redoStack.length = 0;
|
|
76
|
+
undoStack.push({ type: 'canvasReset', snapshot, restore, reset });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function invalidateCanvasResetUndo() {
|
|
80
|
+
for (let index = undoStack.length - 1; index >= 0; index -= 1) {
|
|
81
|
+
if (undoStack[index]?.type === 'canvasReset') undoStack.splice(index, 1);
|
|
82
|
+
}
|
|
83
|
+
for (let index = redoStack.length - 1; index >= 0; index -= 1) {
|
|
84
|
+
if (redoStack[index]?.type === 'canvasReset') redoStack.splice(index, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function clearUndoHistory() {
|
|
89
|
+
undoStack.length = 0;
|
|
90
|
+
redoStack.length = 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
// Enhanced delete action to clean up attached arrows
|
|
69
94
|
export function pushDeleteActionWithAttachments(shape) {
|
|
70
95
|
let affectedArrows = [];
|
|
@@ -442,6 +467,15 @@ export function pushOptionsChangeAction(shape, oldOptions, newOptions = null) {
|
|
|
442
467
|
notifyCollaboration();
|
|
443
468
|
}
|
|
444
469
|
|
|
470
|
+
function applyOptionsSnapshot(shape, options) {
|
|
471
|
+
shape.options = { ...options };
|
|
472
|
+
if (shape.shapeName === 'frame') {
|
|
473
|
+
shape.fillColor = options.fillColor;
|
|
474
|
+
shape.fillStyle = options.fillStyle;
|
|
475
|
+
}
|
|
476
|
+
shape.draw();
|
|
477
|
+
}
|
|
478
|
+
|
|
445
479
|
// Issue #34 bug #4: refresh the multi-selection outline + handles so it
|
|
446
480
|
// follows the reverted geometry. Without this the selection rect stays
|
|
447
481
|
// pinned to the pre-undo coordinates and the user sees a ghost rect
|
|
@@ -458,6 +492,13 @@ export function undo() {
|
|
|
458
492
|
if (undoStack.length === 0) return;
|
|
459
493
|
const action = undoStack.pop();
|
|
460
494
|
try {
|
|
495
|
+
if (action.type === 'canvasReset') {
|
|
496
|
+
action.restore(action.snapshot);
|
|
497
|
+
redoStack.push(action);
|
|
498
|
+
notifyCollaboration();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
461
502
|
if (action.type === 'frameTransform') {
|
|
462
503
|
// Handle frame transformation undo
|
|
463
504
|
action.shape.x = action.oldPos.x;
|
|
@@ -652,7 +693,12 @@ export function undo() {
|
|
|
652
693
|
action.shape.restore();
|
|
653
694
|
} else {
|
|
654
695
|
// Handle other shape deletion undo
|
|
655
|
-
shapes.
|
|
696
|
+
if (shapes.indexOf(action.shape) === -1) {
|
|
697
|
+
const index = Number.isInteger(action.shapeIndex)
|
|
698
|
+
? Math.min(Math.max(action.shapeIndex, 0), shapes.length)
|
|
699
|
+
: shapes.length;
|
|
700
|
+
shapes.splice(index, 0, action.shape);
|
|
701
|
+
}
|
|
656
702
|
if (svg) {
|
|
657
703
|
svg.appendChild(action.shape.group);
|
|
658
704
|
}
|
|
@@ -959,8 +1005,7 @@ export function undo() {
|
|
|
959
1005
|
action.shape.draw();
|
|
960
1006
|
} else {
|
|
961
1007
|
// Handle other shape options change undo
|
|
962
|
-
action.shape
|
|
963
|
-
action.shape.draw();
|
|
1008
|
+
applyOptionsSnapshot(action.shape, action.oldOptions);
|
|
964
1009
|
}
|
|
965
1010
|
redoStack.push(action);
|
|
966
1011
|
return;
|
|
@@ -975,6 +1020,13 @@ export function redo() {
|
|
|
975
1020
|
if (redoStack.length === 0) return;
|
|
976
1021
|
const action = redoStack.pop();
|
|
977
1022
|
try {
|
|
1023
|
+
if (action.type === 'canvasReset') {
|
|
1024
|
+
action.reset();
|
|
1025
|
+
undoStack.push(action);
|
|
1026
|
+
notifyCollaboration();
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
978
1030
|
if (action.type === 'frameTransform') {
|
|
979
1031
|
// Handle frame transformation redo
|
|
980
1032
|
action.shape.x = action.newPos.x;
|
|
@@ -1453,8 +1505,7 @@ export function redo() {
|
|
|
1453
1505
|
action.shape.arrowCurveAmount = action.newOptions.arrowCurveAmount;
|
|
1454
1506
|
action.shape.draw();
|
|
1455
1507
|
} else {
|
|
1456
|
-
action.shape
|
|
1457
|
-
action.shape.draw();
|
|
1508
|
+
applyOptionsSnapshot(action.shape, action.newOptions);
|
|
1458
1509
|
}
|
|
1459
1510
|
undoStack.push(action);
|
|
1460
1511
|
return;
|
|
@@ -1484,6 +1535,9 @@ document.getElementById('redo')?.addEventListener('click', redo);
|
|
|
1484
1535
|
// Expose globally for plain scripts
|
|
1485
1536
|
window.pushCreateAction = pushCreateAction;
|
|
1486
1537
|
window.pushDeleteAction = pushDeleteAction;
|
|
1538
|
+
window.pushCanvasResetAction = pushCanvasResetAction;
|
|
1539
|
+
window.invalidateCanvasResetUndo = invalidateCanvasResetUndo;
|
|
1540
|
+
window.clearUndoHistory = clearUndoHistory;
|
|
1487
1541
|
window.pushDeleteActionWithAttachments = pushDeleteActionWithAttachments;
|
|
1488
1542
|
window.pushTransformAction = pushTransformAction;
|
|
1489
1543
|
window.pushOptionsChangeAction = pushOptionsChangeAction;
|