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);
 |