371 lines
12 KiB
Markdown
371 lines
12 KiB
Markdown
/*
|
||
|
||
format **the left to right** mind map
|
||
|
||

|
||
|
||
# tree
|
||
|
||
Mind map is actually a tree, so you must have a **root node**. The script will determine **the leftmost element** of the selected element as the root element (node is excalidraw element, e.g. rectangle, diamond, ellipse, text, image, but it can't be arrow, line, freedraw, **group**)
|
||
|
||
The element connecting node and node must be an **arrow** and have the correct direction, e.g. **parent node -> children node**
|
||
|
||
# sort
|
||
|
||
The order of nodes in the Y axis or vertical direction is determined by **the creation time** of the arrow connecting it
|
||
|
||

|
||
|
||
So if you want to readjust the order, you can **delete arrows and reconnect them**
|
||
|
||
# setting
|
||
|
||
Script provides options to adjust the style of mind map, The option is at the bottom of the option of the exalidraw plugin(e.g. Settings -> Community plugins -> Excalidraw -> drag to bottom)
|
||
|
||
# problem
|
||
|
||
1. since the start bingding and end bingding of the arrow are easily disconnected from the node, so if there are unformatted parts, please **check the connection** and use the script to **reformat**
|
||
|
||
```javascript
|
||
*/
|
||
|
||
let settings = ea.getScriptSettings();
|
||
//set default values on first run
|
||
if (!settings["MindMap Format"]) {
|
||
settings = {
|
||
"MindMap Format": {
|
||
value: "Excalidraw/MindMap Format",
|
||
description:
|
||
"This is prepared for the namespace of MindMap Format and does not need to be modified",
|
||
},
|
||
"default gap": {
|
||
value: 10,
|
||
description: "Interval size of element",
|
||
},
|
||
"curve length": {
|
||
value: 40,
|
||
description: "The length of the curve part in the mind map line",
|
||
},
|
||
"length between element and line": {
|
||
value: 50,
|
||
description:
|
||
"The distance between the tail of the connection and the connecting elements of the mind map",
|
||
},
|
||
};
|
||
ea.setScriptSettings(settings);
|
||
}
|
||
|
||
const sceneElements = ea.getExcalidrawAPI().getSceneElements();
|
||
|
||
// default X coordinate of the middle point of the arc
|
||
const defaultDotX = Number(settings["curve length"].value);
|
||
// The default length from the middle point of the arc on the X axis
|
||
const defaultLengthWithCenterDot = Number(
|
||
settings["length between element and line"].value
|
||
);
|
||
// Initial trimming distance of the end point on the Y axis
|
||
const initAdjLength = 4;
|
||
// default gap
|
||
const defaultGap = Number(settings["default gap"].value);
|
||
|
||
const setCenter = (parent, line) => {
|
||
// Focus and gap need the api calculation of excalidraw
|
||
// e.g. determineFocusDistance, but they are not available now
|
||
// so they are uniformly set to 0/1
|
||
line.startBinding.focus = 0;
|
||
line.startBinding.gap = 1;
|
||
line.endBinding.focus = 0;
|
||
line.endBinding.gap = 1;
|
||
line.x = parent.x + parent.width;
|
||
line.y = parent.y + parent.height / 2;
|
||
};
|
||
|
||
/**
|
||
* set the middle point of curve
|
||
* @param {any} lineEl the line element of excalidraw
|
||
* @param {number} height height of dot on Y axis
|
||
* @param {number} [ratio=1] ,coefficient of the initial trimming distance of the end point on the Y axis, default is 1
|
||
*/
|
||
const setTopCurveDotOnLine = (lineEl, height, ratio = 1) => {
|
||
if (lineEl.points.length < 3) {
|
||
lineEl.points.splice(1, 0, [defaultDotX, lineEl.points[0][1] - height]);
|
||
} else if (lineEl.points.length === 3) {
|
||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] - height];
|
||
} else {
|
||
lineEl.points.splice(2, lineEl.points.length - 3);
|
||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] - height];
|
||
}
|
||
lineEl.points[2][0] = lineEl.points[1][0] + defaultLengthWithCenterDot;
|
||
// adjust the curvature of the second line segment
|
||
lineEl.points[2][1] = lineEl.points[1][1] - initAdjLength * ratio * 0.8;
|
||
};
|
||
|
||
const setMidCurveDotOnLine = (lineEl) => {
|
||
if (lineEl.points.length < 3) {
|
||
lineEl.points.splice(1, 0, [defaultDotX, lineEl.points[0][1]]);
|
||
} else if (lineEl.points.length === 3) {
|
||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1]];
|
||
} else {
|
||
lineEl.points.splice(2, lineEl.points.length - 3);
|
||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1]];
|
||
}
|
||
lineEl.points[2][0] = lineEl.points[1][0] + defaultLengthWithCenterDot;
|
||
lineEl.points[2][1] = lineEl.points[1][1];
|
||
};
|
||
|
||
/**
|
||
* set the middle point of curve
|
||
* @param {any} lineEl the line element of excalidraw
|
||
* @param {number} height height of dot on Y axis
|
||
* @param {number} [ratio=1] ,coefficient of the initial trimming distance of the end point on the Y axis, default is 1
|
||
*/
|
||
const setBottomCurveDotOnLine = (lineEl, height, ratio = 1) => {
|
||
if (lineEl.points.length < 3) {
|
||
lineEl.points.splice(1, 0, [defaultDotX, lineEl.points[0][1] + height]);
|
||
} else if (lineEl.points.length === 3) {
|
||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] + height];
|
||
} else {
|
||
lineEl.points.splice(2, lineEl.points.length - 3);
|
||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] + height];
|
||
}
|
||
lineEl.points[2][0] = lineEl.points[1][0] + defaultLengthWithCenterDot;
|
||
// adjust the curvature of the second line segment
|
||
lineEl.points[2][1] = lineEl.points[1][1] + initAdjLength * ratio * 0.8;
|
||
};
|
||
|
||
const setTextXY = (rect, text) => {
|
||
text.x = rect.x + (rect.width - text.width) / 2;
|
||
text.y = rect.y + (rect.height - text.height) / 2;
|
||
};
|
||
|
||
const setChildrenXY = (parent, children, line, elementsMap) => {
|
||
x = parent.x + parent.width + line.points[2][0];
|
||
y = parent.y + parent.height / 2 + line.points[2][1] - children.height / 2;
|
||
distX = children.x - x;
|
||
distY = children.y - y;
|
||
|
||
ea.getElementsInTheSameGroupWithElement(children, sceneElements).forEach((el) => {
|
||
el.x = el.x - distX;
|
||
el.y = el.y - distY;
|
||
});
|
||
|
||
if (
|
||
["rectangle", "diamond", "ellipse"].includes(children.type) &&
|
||
![null, undefined].includes(children.boundElements)
|
||
) {
|
||
const textDesc = children.boundElements.filter(
|
||
(el) => el.type === "text"
|
||
)[0];
|
||
if (textDesc !== undefined) {
|
||
const textEl = elementsMap.get(textDesc.id);
|
||
setTextXY(children, textEl);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* returns the height of the upper part of all child nodes
|
||
* and the height of the lower part of all child nodes
|
||
* @param {Number[]} childrenTotalHeightArr
|
||
* @returns {Number[]} [topHeight, bottomHeight]
|
||
*/
|
||
const getNodeCurrentHeight = (childrenTotalHeightArr) => {
|
||
if (childrenTotalHeightArr.length <= 0) return [0, 0];
|
||
else if (childrenTotalHeightArr.length === 1)
|
||
return [childrenTotalHeightArr[0] / 2, childrenTotalHeightArr[0] / 2];
|
||
const heightArr = childrenTotalHeightArr;
|
||
let topHeight = 0,
|
||
bottomHeight = 0;
|
||
const isEven = heightArr.length % 2 === 0;
|
||
const mid = Math.floor(heightArr.length / 2);
|
||
const topI = mid - 1;
|
||
const bottomI = isEven ? mid : mid + 1;
|
||
topHeight = isEven ? 0 : heightArr[mid] / 2;
|
||
for (let i = topI; i >= 0; i--) {
|
||
topHeight += heightArr[i];
|
||
}
|
||
bottomHeight = isEven ? 0 : heightArr[mid] / 2;
|
||
for (let i = bottomI; i < heightArr.length; i++) {
|
||
bottomHeight += heightArr[i];
|
||
}
|
||
return [topHeight, bottomHeight];
|
||
};
|
||
|
||
/**
|
||
* handle the height of each point in the single-level tree
|
||
* @param {Array} lines
|
||
* @param {Map} elementsMap
|
||
* @param {Boolean} isEven
|
||
* @param {Number} mid 'lines' array midpoint index
|
||
* @returns {Array} height array corresponding to 'lines'
|
||
*/
|
||
const handleDotYValue = (lines, elementsMap, isEven, mid) => {
|
||
const getTotalHeight = (line, elementsMap) => {
|
||
return elementsMap.get(line.endBinding.elementId).totalHeight;
|
||
};
|
||
const getTopHeight = (line, elementsMap) => {
|
||
return elementsMap.get(line.endBinding.elementId).topHeight;
|
||
};
|
||
const getBottomHeight = (line, elementsMap) => {
|
||
return elementsMap.get(line.endBinding.elementId).bottomHeight;
|
||
};
|
||
const heightArr = new Array(lines.length).fill(0);
|
||
const upI = mid === 0 ? 0 : mid - 1;
|
||
const bottomI = isEven ? mid : mid + 1;
|
||
let initHeight = isEven ? 0 : getTopHeight(lines[mid], elementsMap);
|
||
for (let i = upI; i >= 0; i--) {
|
||
heightArr[i] = initHeight + getBottomHeight(lines[i], elementsMap);
|
||
initHeight += getTotalHeight(lines[i], elementsMap);
|
||
}
|
||
initHeight = isEven ? 0 : getBottomHeight(lines[mid], elementsMap);
|
||
for (let i = bottomI; i < lines.length; i++) {
|
||
heightArr[i] = initHeight + getTopHeight(lines[i], elementsMap);
|
||
initHeight += getTotalHeight(lines[i], elementsMap);
|
||
}
|
||
return heightArr;
|
||
};
|
||
|
||
/**
|
||
* format single-level tree
|
||
* @param {any} parent
|
||
* @param {Array} lines
|
||
* @param {Map} childrenDescMap
|
||
* @param {Map} elementsMap
|
||
*/
|
||
const formatTree = (parent, lines, childrenDescMap, elementsMap) => {
|
||
lines.forEach((item) => setCenter(parent, item));
|
||
|
||
const isEven = lines.length % 2 === 0;
|
||
const mid = Math.floor(lines.length / 2);
|
||
const heightArr = handleDotYValue(lines, childrenDescMap, isEven, mid);
|
||
lines.forEach((item, index) => {
|
||
if (isEven) {
|
||
if (index < mid) setTopCurveDotOnLine(item, heightArr[index], index + 1);
|
||
else setBottomCurveDotOnLine(item, heightArr[index], index - mid + 1);
|
||
} else {
|
||
if (index < mid) setTopCurveDotOnLine(item, heightArr[index], index + 1);
|
||
else if (index === mid) setMidCurveDotOnLine(item);
|
||
else setBottomCurveDotOnLine(item, heightArr[index], index - mid);
|
||
}
|
||
});
|
||
lines.forEach((item) => {
|
||
if (item.endBinding !== null) {
|
||
setChildrenXY(
|
||
parent,
|
||
elementsMap.get(item.endBinding.elementId),
|
||
item,
|
||
elementsMap
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
const generateTree = (elements) => {
|
||
const elIdMap = new Map([[elements[0].id, elements[0]]]);
|
||
let minXEl = elements[0];
|
||
for (let i = 1; i < elements.length; i++) {
|
||
elIdMap.set(elements[i].id, elements[i]);
|
||
if (
|
||
!(elements[i].type === "arrow" || elements[i].type === "line") &&
|
||
elements[i].x < minXEl.x
|
||
) {
|
||
minXEl = elements[i];
|
||
}
|
||
}
|
||
const root = {
|
||
el: minXEl,
|
||
totalHeight: minXEl.height,
|
||
topHeight: 0,
|
||
bottomHeight: 0,
|
||
linkChildrensLines: [],
|
||
isLeafNode: false,
|
||
children: [],
|
||
};
|
||
const preIdSet = new Set(); // The id_set of Elements that is already in the tree, avoid a dead cycle
|
||
const dfsForTreeData = (root) => {
|
||
if (preIdSet.has(root.el.id)) {
|
||
return 0;
|
||
}
|
||
preIdSet.add(root.el.id);
|
||
let lines = root.el.boundElements.filter(
|
||
(el) =>
|
||
el.type === "arrow" &&
|
||
!preIdSet.has(el.id) &&
|
||
elIdMap.get(el.id)?.startBinding?.elementId === root.el.id
|
||
);
|
||
if (lines.length === 0) {
|
||
root.isLeafNode = true;
|
||
root.totalHeight = root.el.height + 2 * defaultGap;
|
||
[root.topHeight, root.bottomHeight] = [
|
||
root.totalHeight / 2,
|
||
root.totalHeight / 2,
|
||
];
|
||
return root.totalHeight;
|
||
} else {
|
||
lines = lines.map((elementDesc) => {
|
||
preIdSet.add(elementDesc.id);
|
||
return elIdMap.get(elementDesc.id);
|
||
});
|
||
}
|
||
|
||
const linkChildrensLines = [];
|
||
lines.forEach((el) => {
|
||
const line = el;
|
||
if (
|
||
line &&
|
||
line.endBinding !== null &&
|
||
line.endBinding !== undefined &&
|
||
!preIdSet.has(elIdMap.get(line.endBinding.elementId).id)
|
||
) {
|
||
const children = elIdMap.get(line.endBinding.elementId);
|
||
linkChildrensLines.push(line);
|
||
root.children.push({
|
||
el: children,
|
||
totalHeight: 0,
|
||
topHeight: 0,
|
||
bottomHeight: 0,
|
||
linkChildrensLines: [],
|
||
isLeafNode: false,
|
||
children: [],
|
||
});
|
||
}
|
||
});
|
||
|
||
let totalHeight = 0;
|
||
root.children.forEach((el) => (totalHeight += dfsForTreeData(el)));
|
||
|
||
root.linkChildrensLines = linkChildrensLines;
|
||
if (root.children.length === 0) {
|
||
root.isLeafNode = true;
|
||
root.totalHeight = root.el.height + 2 * defaultGap;
|
||
[root.topHeight, root.bottomHeight] = [
|
||
root.totalHeight / 2,
|
||
root.totalHeight / 2,
|
||
];
|
||
} else if (root.children.length > 0) {
|
||
root.totalHeight = Math.max(root.el.height + 2 * defaultGap, totalHeight);
|
||
[root.topHeight, root.bottomHeight] = getNodeCurrentHeight(
|
||
root.children.map((item) => item.totalHeight)
|
||
);
|
||
}
|
||
|
||
return totalHeight;
|
||
};
|
||
dfsForTreeData(root);
|
||
const dfsForFormat = (root) => {
|
||
if (root.isLeafNode) return;
|
||
const childrenDescMap = new Map(
|
||
root.children.map((item) => [item.el.id, item])
|
||
);
|
||
formatTree(root.el, root.linkChildrensLines, childrenDescMap, elIdMap);
|
||
root.children.forEach((el) => dfsForFormat(el));
|
||
};
|
||
dfsForFormat(root);
|
||
};
|
||
|
||
const elements = ea.getViewSelectedElements();
|
||
generateTree(elements);
|
||
|
||
ea.copyViewElementsToEAforEditing(elements);
|
||
await ea.addElementsToView(false, false);
|