@elixpo/lixsketch 5.6.5 → 5.6.7
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-PPDS3AD6.js} +11 -10
- package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-PPDS3AD6.js.map} +1 -1
- package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-UV7LFSAS.js} +216 -63
- package/dist/react/EventDispatcher-UV7LFSAS.js.map +7 -0
- package/dist/react/Frame-DELCMYV6.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-PZHMZSLR.js +10 -0
- package/dist/react/ImageShape-ZMBZARRI.js +10 -0
- package/dist/react/Line-VPVPL6ET.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-H6KMQCO2.js} +120 -46
- package/dist/react/SceneSerializer-H6KMQCO2.js.map +7 -0
- package/dist/react/{Selection-2TSH3J6P.js → Selection-CFWRGVKH.js} +4 -4
- package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-UFLZJQXS.js} +26 -26
- package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-RXO7YHA2.js} +12 -2
- package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-DO327FKU.js} +3 -3
- package/dist/react/{chunk-EOURRJ55.js → chunk-2ALS5JGS.js} +2 -2
- package/dist/react/{chunk-LJR7EVW2.js → chunk-37PF7FG2.js} +3 -3
- package/dist/react/chunk-6AIFXYV6.js +31 -0
- package/dist/react/chunk-6AIFXYV6.js.map +7 -0
- package/dist/react/{chunk-XOWNCYE4.js → chunk-6LWUR7XB.js} +3 -3
- package/dist/react/{chunk-ASBC5G35.js → chunk-7DD2JRBC.js} +2 -2
- package/dist/react/{chunk-OP56XPWF.js → chunk-BOZEDUAF.js} +2 -2
- package/dist/react/{chunk-RGMDWLRJ.js → chunk-CGMUXYPJ.js} +3 -3
- package/dist/react/{chunk-M6TMN76K.js → chunk-DODPOWNX.js} +3 -3
- package/dist/react/{chunk-LSHZYN77.js → chunk-GQLXUZHQ.js} +2 -2
- package/dist/react/{chunk-7CM5XQDN.js → chunk-I7TVGJFF.js} +20 -36
- package/dist/react/chunk-I7TVGJFF.js.map +7 -0
- package/dist/react/{chunk-K5RY4EV7.js → chunk-KC26KJW4.js} +26 -17
- package/dist/react/chunk-KC26KJW4.js.map +7 -0
- package/dist/react/{chunk-XFL4R5ZF.js → chunk-KZBKYHZO.js} +16 -35
- package/dist/react/chunk-KZBKYHZO.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-APTOPAOY.js → chunk-O7KXLFZN.js} +3 -3
- package/dist/react/{chunk-GQUZPON4.js → chunk-RK3NVAER.js} +4 -4
- package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
- package/dist/react/chunk-VZPH7OAM.js.map +7 -0
- package/dist/react/{chunk-6GPEWDRB.js → chunk-VZX55VQL.js} +115 -6
- package/dist/react/chunk-VZX55VQL.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-XYUFEIZR.js → chunk-X5ENBMGL.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-NK4RVDAR.js} +4 -4
- package/dist/react/{codeTool-4YZO5W7J.js → codeTool-3WF4HEOW.js} +4 -4
- package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-VCNV2NG2.js} +4 -4
- package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-TBAOD46K.js} +4 -4
- package/dist/react/{iconTool-46O5VEUX.js → iconTool-65JCAKYF.js} +4 -4
- package/dist/react/{imageTool-P2RWZRHO.js → imageTool-UQZUBOK2.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-762KBYOM.js} +4 -4
- package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-IQK3HZK6.js} +4 -4
- package/dist/react/{textTool-S4XXZHZH.js → textTool-ITG2SBJK.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 +29 -14
- package/src/core/UndoRedo.js +126 -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-K5RY4EV7.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-DELCMYV6.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-PZHMZSLR.js.map} +0 -0
- /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-ZMBZARRI.js.map} +0 -0
- /package/dist/react/{Line-APEULMHE.js.map → Line-VPVPL6ET.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-CFWRGVKH.js.map} +0 -0
- /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-UFLZJQXS.js.map} +0 -0
- /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-RXO7YHA2.js.map} +0 -0
- /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-DO327FKU.js.map} +0 -0
- /package/dist/react/{chunk-EOURRJ55.js.map → chunk-2ALS5JGS.js.map} +0 -0
- /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-37PF7FG2.js.map} +0 -0
- /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-6LWUR7XB.js.map} +0 -0
- /package/dist/react/{chunk-ASBC5G35.js.map → chunk-7DD2JRBC.js.map} +0 -0
- /package/dist/react/{chunk-OP56XPWF.js.map → chunk-BOZEDUAF.js.map} +0 -0
- /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-CGMUXYPJ.js.map} +0 -0
- /package/dist/react/{chunk-M6TMN76K.js.map → chunk-DODPOWNX.js.map} +0 -0
- /package/dist/react/{chunk-LSHZYN77.js.map → chunk-GQLXUZHQ.js.map} +0 -0
- /package/dist/react/{chunk-APTOPAOY.js.map → chunk-O7KXLFZN.js.map} +0 -0
- /package/dist/react/{chunk-GQUZPON4.js.map → chunk-RK3NVAER.js.map} +0 -0
- /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-X5ENBMGL.js.map} +0 -0
- /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-NK4RVDAR.js.map} +0 -0
- /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-3WF4HEOW.js.map} +0 -0
- /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-VCNV2NG2.js.map} +0 -0
- /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-TBAOD46K.js.map} +0 -0
- /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-65JCAKYF.js.map} +0 -0
- /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-UQZUBOK2.js.map} +0 -0
- /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-762KBYOM.js.map} +0 -0
- /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-IQK3HZK6.js.map} +0 -0
- /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-ITG2SBJK.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-CGMUXYPJ.js";
|
|
7
|
+
import "./chunk-X5ENBMGL.js";
|
|
8
|
+
import "./chunk-VZX55VQL.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-762KBYOM.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-I7TVGJFF.js";
|
|
7
|
+
import "./chunk-X5ENBMGL.js";
|
|
8
|
+
import "./chunk-VZX55VQL.js";
|
|
9
9
|
export {
|
|
10
10
|
handleMouseDownRect,
|
|
11
11
|
handleMouseMoveRect,
|
|
12
12
|
handleMouseUpRect
|
|
13
13
|
};
|
|
14
|
-
//# sourceMappingURL=rectangleTool-
|
|
14
|
+
//# sourceMappingURL=rectangleTool-IQK3HZK6.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-RK3NVAER.js";
|
|
10
|
+
import "./chunk-DODPOWNX.js";
|
|
11
11
|
import "./chunk-PAKA6HUX.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-X5ENBMGL.js";
|
|
13
|
+
import "./chunk-VZX55VQL.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-ITG2SBJK.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.7",
|
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Multi-selection system - copied from selection.js
|
|
3
3
|
|
|
4
4
|
import { cleanupAttachments } from '../tools/arrowTool.js';
|
|
5
|
-
import { pushCreateAction, pushTransformAction, pushFrameAttachmentAction, pushDeleteAction } from './UndoRedo.js';
|
|
5
|
+
import { beginUndoBatch, endUndoBatch, pushCreateAction, pushTransformAction, pushFrameAttachmentAction, pushDeleteAction } from './UndoRedo.js';
|
|
6
6
|
import { calculateSnap, clearSnapGuides } from './SnapGuides.js';
|
|
7
7
|
import { registerRotationAnchor } from './ScreenSpaceControls.js';
|
|
8
8
|
|
|
@@ -278,19 +278,21 @@ class MultiSelection {
|
|
|
278
278
|
endPoint: { ...shape.endPoint },
|
|
279
279
|
x: shape.x, y: shape.y,
|
|
280
280
|
width: shape.width || 0, height: shape.height || 0,
|
|
281
|
-
rotation: shape.rotation || 0
|
|
281
|
+
rotation: shape.rotation || 0,
|
|
282
|
+
parentFrame: shape.parentFrame || null
|
|
282
283
|
};
|
|
283
284
|
case 'circle':
|
|
284
|
-
return { x: shape.x, y: shape.y, rx: shape.rx, ry: shape.ry, rotation: shape.rotation || 0, width: shape.width, height: shape.height };
|
|
285
|
+
return { x: shape.x, y: shape.y, rx: shape.rx, ry: shape.ry, rotation: shape.rotation || 0, width: shape.width, height: shape.height, parentFrame: shape.parentFrame || null };
|
|
285
286
|
case 'freehandStroke':
|
|
286
|
-
return { x: shape.x, y: shape.y, width: shape.width, height: shape.height, rotation: shape.rotation || 0, points: JSON.parse(JSON.stringify(shape.points)) };
|
|
287
|
+
return { x: shape.x, y: shape.y, width: shape.width, height: shape.height, rotation: shape.rotation || 0, points: JSON.parse(JSON.stringify(shape.points)), parentFrame: shape.parentFrame || null };
|
|
287
288
|
default:
|
|
288
|
-
return { x: shape.x || 0, y: shape.y || 0, width: shape.width || 0, height: shape.height || 0, rotation: shape.rotation || 0 };
|
|
289
|
+
return { x: shape.x || 0, y: shape.y || 0, width: shape.width || 0, height: shape.height || 0, rotation: shape.rotation || 0, parentFrame: shape.parentFrame || null };
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
// Push undo actions for all shapes that changed
|
|
293
|
-
_pushUndoForAll() {
|
|
294
|
+
_pushUndoForAll(existingBatch = null) {
|
|
295
|
+
const batch = existingBatch || beginUndoBatch();
|
|
294
296
|
this.selectedShapes.forEach(shape => {
|
|
295
297
|
const oldState = this._undoSnapshots.get(shape);
|
|
296
298
|
if (!oldState) return;
|
|
@@ -305,6 +307,7 @@ class MultiSelection {
|
|
|
305
307
|
}
|
|
306
308
|
});
|
|
307
309
|
this._undoSnapshots.clear();
|
|
310
|
+
if (!existingBatch) endUndoBatch(batch, 'multi-transform');
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
addShape(shape) {
|
|
@@ -1392,6 +1395,8 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1392
1395
|
shape.isSelected = true;
|
|
1393
1396
|
});
|
|
1394
1397
|
|
|
1398
|
+
const undoBatch = beginUndoBatch();
|
|
1399
|
+
|
|
1395
1400
|
// Re-parent any selected shape that now sits inside a different frame
|
|
1396
1401
|
// (Issue #22, bug #9). The per-tool drag handlers (rectangleTool, etc.)
|
|
1397
1402
|
// do this for single-shape drags, but a multi-selection drag bypassed
|
|
@@ -1418,11 +1423,6 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1418
1423
|
}
|
|
1419
1424
|
if (target && typeof target.addShapeToFrame === 'function') {
|
|
1420
1425
|
target.addShapeToFrame(shape);
|
|
1421
|
-
if (typeof pushFrameAttachmentAction === 'function') {
|
|
1422
|
-
pushFrameAttachmentAction(target, shape, 'attach', current);
|
|
1423
|
-
}
|
|
1424
|
-
} else if (current && typeof pushFrameAttachmentAction === 'function') {
|
|
1425
|
-
pushFrameAttachmentAction(current, shape, 'detach', current);
|
|
1426
1426
|
}
|
|
1427
1427
|
});
|
|
1428
1428
|
}
|
|
@@ -1430,7 +1430,8 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1430
1430
|
this.initialPositions.clear();
|
|
1431
1431
|
|
|
1432
1432
|
// Push undo for all moved shapes
|
|
1433
|
-
this._pushUndoForAll();
|
|
1433
|
+
this._pushUndoForAll(undoBatch);
|
|
1434
|
+
endUndoBatch(undoBatch, 'multi-move');
|
|
1434
1435
|
|
|
1435
1436
|
// Refresh bounds and controls after finalizeMove may have changed positions
|
|
1436
1437
|
this.updateControls();
|
|
@@ -1864,8 +1865,19 @@ document.addEventListener('keydown', (e) => {
|
|
|
1864
1865
|
function deleteSelectedShapes() {
|
|
1865
1866
|
if (multiSelection.selectedShapes.size === 0) return;
|
|
1866
1867
|
|
|
1868
|
+
if (
|
|
1869
|
+
Array.isArray(window.shapes) &&
|
|
1870
|
+
window.shapes.length > 0 &&
|
|
1871
|
+
multiSelection.selectedShapes.size >= window.shapes.length &&
|
|
1872
|
+
typeof window.__requestCanvasResetConfirmation === 'function'
|
|
1873
|
+
) {
|
|
1874
|
+
window.__requestCanvasResetConfirmation();
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1867
1878
|
const shapesToDelete = Array.from(multiSelection.selectedShapes);
|
|
1868
1879
|
multiSelection.clearSelection();
|
|
1880
|
+
const undoBatch = beginUndoBatch();
|
|
1869
1881
|
|
|
1870
1882
|
// Issue #24 bug #3: when deleting a (regular, non-diagram) frame, its
|
|
1871
1883
|
// children are released back to the canvas — capture them here so we
|
|
@@ -1876,6 +1888,8 @@ function deleteSelectedShapes() {
|
|
|
1876
1888
|
shapesToDelete.forEach(shape => {
|
|
1877
1889
|
const isFrame = shape.shapeName === 'frame';
|
|
1878
1890
|
const isDiagramFrame = isFrame && !!shape._diagramType;
|
|
1891
|
+
const parentFrame = shape.parentFrame || null;
|
|
1892
|
+
const frameIndex = parentFrame?.containedShapes?.indexOf(shape) ?? -1;
|
|
1879
1893
|
|
|
1880
1894
|
// Issue #24 bug #7: for a regular (non-diagram) frame, we don't
|
|
1881
1895
|
// want destroy() — it empties containedShapes and tears down the
|
|
@@ -1938,14 +1952,15 @@ function deleteSelectedShapes() {
|
|
|
1938
1952
|
// snapshot so undo can re-attach them.
|
|
1939
1953
|
try {
|
|
1940
1954
|
if (childSnapshot) {
|
|
1941
|
-
pushDeleteAction(shape, { childSnapshot });
|
|
1955
|
+
pushDeleteAction(shape, { childSnapshot, shapeIndex: idx, parentFrame, frameIndex });
|
|
1942
1956
|
} else {
|
|
1943
|
-
pushDeleteAction(shape);
|
|
1957
|
+
pushDeleteAction(shape, { shapeIndex: idx, parentFrame, frameIndex });
|
|
1944
1958
|
}
|
|
1945
1959
|
} catch (err) {
|
|
1946
1960
|
console.warn('[deleteSelectedShapes] pushDeleteAction failed:', err);
|
|
1947
1961
|
}
|
|
1948
1962
|
});
|
|
1963
|
+
endUndoBatch(undoBatch, 'multi-delete');
|
|
1949
1964
|
|
|
1950
1965
|
// Bug #3 follow-through: auto-select the released children and switch
|
|
1951
1966
|
// to the pointer tool so the user can immediately keep manipulating
|