diff --git a/ea-scripts/Auto Layout.md b/ea-scripts/Auto Layout.md new file mode 100644 index 0000000..268646e --- /dev/null +++ b/ea-scripts/Auto Layout.md @@ -0,0 +1,407 @@ +/* + +![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg) + +Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian. + +![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-auto-layout.png) + +This script performs automatic layout for the selected top-level grouping objects. It is powered by [elkjs](https://github.com/kieler/elkjs) and needs to be connected to the Internet. + + +See documentation for more details: +https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html + +```javascript +*/ + +if ( + !ea.verifyMinimumPluginVersion || + !ea.verifyMinimumPluginVersion("1.5.21") +) { + new Notice( + "This script requires a newer version of Excalidraw. Please install the latest version." + ); + return; +} + +settings = ea.getScriptSettings(); +//set default values on first run +if (!settings["Layout Options JSON"]) { + settings = { + "Layout Options JSON": { + height: "450px", + value: `{\n "org.eclipse.elk.layered.crossingMinimization.semiInteractive": "true",\n "org.eclipse.elk.layered.considerModelOrder.components": "FORCE_MODEL_ORDER"\n}`, + description: `You can use layout options to configure the layout algorithm. A list of all options and further details of their exact effects is available in ELK's documentation.`, + }, + }; + ea.setScriptSettings(settings); +} + +if (typeof ELK === "undefined") { + loadELK(doAutoLayout); +} else { + doAutoLayout(); +} + +async function doAutoLayout() { + const selectedElements = ea.getViewSelectedElements(); + const groups = ea + .getMaximumGroups(selectedElements) + .map((g) => g.filter((el) => el.containerId == null)) // ignore text in stickynote + .filter((els) => els.length > 0); + + const stickynotesMap = selectedElements + .filter((el) => el.containerId != null) + .reduce((result, el) => { + result.set(el.containerId, el); + return result; + }, new Map()); + + const elk = new ELK(); + const knownLayoutAlgorithms = await elk.knownLayoutAlgorithms(); + const layoutAlgorithms = knownLayoutAlgorithms + .map((knownLayoutAlgorithm) => ({ + id: knownLayoutAlgorithm.id, + displayText: + knownLayoutAlgorithm.id === "org.eclipse.elk.layered" || + knownLayoutAlgorithm.id === "org.eclipse.elk.radial" || + knownLayoutAlgorithm.id === "org.eclipse.elk.mrtree" + ? "* " + + knownLayoutAlgorithm.name + + ": " + + knownLayoutAlgorithm.description + : knownLayoutAlgorithm.name + ": " + knownLayoutAlgorithm.description, + })) + .sort((lha, rha) => lha.displayText.localeCompare(rha.displayText)); + + const layoutAlgorithmsSimple = knownLayoutAlgorithms + .map((knownLayoutAlgorithm) => ({ + id: knownLayoutAlgorithm.id, + displayText: + knownLayoutAlgorithm.id === "org.eclipse.elk.layered" || + knownLayoutAlgorithm.id === "org.eclipse.elk.radial" || + knownLayoutAlgorithm.id === "org.eclipse.elk.mrtree" + ? "* " + knownLayoutAlgorithm.name + : knownLayoutAlgorithm.name, + })) + .sort((lha, rha) => lha.displayText.localeCompare(rha.displayText)); + + // const knownOptions = knownLayoutAlgorithms + // .reduce( + // (result, knownLayoutAlgorithm) => [ + // ...result, + // ...knownLayoutAlgorithm.knownOptions, + // ], + // [] + // ) + // .filter((value, index, self) => self.indexOf(value) === index) // remove duplicates + // .sort((lha, rha) => lha.localeCompare(rha)); + // console.log("knownOptions", knownOptions); + + const selectedAlgorithm = await utils.suggester( + layoutAlgorithms.map((algorithmInfo) => algorithmInfo.displayText), + layoutAlgorithms.map((algorithmInfo) => algorithmInfo.id), + "Layout algorithm" + ); + + const knownNodePlacementStrategy = [ + "SIMPLE", + "INTERACTIVE", + "LINEAR_SEGMENTS", + "BRANDES_KOEPF", + "NETWORK_SIMPLEX", + ]; + + const knownDirections = [ + "UNDEFINED", + "RIGHT", + "LEFT", + "DOWN", + "UP" + ]; + + let nodePlacementStrategy = "BRANDES_KOEPF"; + let componentComponentSpacing = "10"; + let nodeNodeSpacing = "100"; + let nodeNodeBetweenLayersSpacing = "100"; + let discoComponentLayoutAlgorithm = "org.eclipse.elk.layered"; + let direction = "UNDEFINED"; + + if (selectedAlgorithm === "org.eclipse.elk.layered") { + nodePlacementStrategy = await utils.suggester( + knownNodePlacementStrategy, + knownNodePlacementStrategy, + "Node placement strategy" + ); + + selectedDirection = await utils.suggester( + knownDirections, + knownDirections, + "Direction" + ); + direction = selectedDirection??"UNDEFINED"; + } else if (selectedAlgorithm === "org.eclipse.elk.disco") { + const componentLayoutAlgorithms = layoutAlgorithmsSimple.filter(al => al.id !== "org.eclipse.elk.disco"); + const selectedDiscoComponentLayoutAlgorithm = await utils.suggester( + componentLayoutAlgorithms.map((algorithmInfo) => algorithmInfo.displayText), + componentLayoutAlgorithms.map((algorithmInfo) => algorithmInfo.id), + "Disco Connected Components Layout Algorithm" + ); + discoComponentLayoutAlgorithm = selectedDiscoComponentLayoutAlgorithm??"org.eclipse.elk.layered"; + } + + if ( + selectedAlgorithm === "org.eclipse.elk.box" || + selectedAlgorithm === "org.eclipse.elk.rectpacking" + ) { + nodeNodeSpacing = await utils.inputPrompt("Node Spacing", "number", "10"); + } else { + let userSpacingStr = await utils.inputPrompt( + "Components Spacing, Node Spacing, Node Node Between Layers Spacing", + "number, number, number", + "10, 100, 100" + ); + let userSpacingArr = (userSpacingStr??"").split(","); + componentComponentSpacing = userSpacingArr[0] ?? "10"; + nodeNodeSpacing = userSpacingArr[1] ?? "100"; + nodeNodeBetweenLayersSpacing = userSpacingArr[2] ?? "100"; + } + + let layoutOptionsJson = {}; + try { + layoutOptionsJson = JSON.parse(settings["Layout Options JSON"].value); + } catch (e) { + new Notice( + "Error reading Layout Options JSON, see developer console for more information", + 4000 + ); + console.log(e); + } + + layoutOptionsJson["elk.algorithm"] = selectedAlgorithm; + layoutOptionsJson["org.eclipse.elk.spacing.componentComponent"] = + componentComponentSpacing; + layoutOptionsJson["org.eclipse.elk.spacing.nodeNode"] = nodeNodeSpacing; + layoutOptionsJson["org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers"] = + nodeNodeBetweenLayersSpacing; + layoutOptionsJson["org.eclipse.elk.layered.nodePlacement.strategy"] = + nodePlacementStrategy; + layoutOptionsJson["org.eclipse.elk.disco.componentCompaction.componentLayoutAlgorithm"] = + discoComponentLayoutAlgorithm; + layoutOptionsJson["org.eclipse.elk.direction"] = direction; + + const graph = { + id: "root", + layoutOptions: layoutOptionsJson, + children: [], + edges: [], + }; + + let groupMap = new Map(); + let targetElkMap = new Map(); + let arrowEls = []; + + for (let i = 0; i < groups.length; i++) { + const elements = groups[i]; + if ( + elements.length === 1 && + (elements[0].type === "arrow" || elements[0].type === "line") + ) { + if ( + elements[0].type === "arrow" && + elements[0].startBinding && + elements[0].endBinding + ) { + arrowEls.push(elements[0]); + } + } else { + let elkId = "g" + i; + elements.reduce((result, el) => { + result.set(el.id, elkId); + return result; + }, targetElkMap); + + const box = ea.getBoundingBox(elements); + groupMap.set(elkId, { + elements: elements, + boundingBox: box, + }); + + graph.children.push({ + id: elkId, + width: box.width, + height: box.height, + x: box.topX, + y: box.topY, + }); + } + } + + for (let i = 0; i < arrowEls.length; i++) { + const arrowEl = arrowEls[i]; + const startElkId = targetElkMap.get(arrowEl.startBinding.elementId); + const endElkId = targetElkMap.get(arrowEl.endBinding.elementId); + + graph.edges.push({ + id: "e" + i, + sources: [startElkId], + targets: [endElkId], + }); + } + + const initTopX = + Math.min(...Array.from(groupMap.values()).map((v) => v.boundingBox.topX)) - + 12; + const initTopY = + Math.min(...Array.from(groupMap.values()).map((v) => v.boundingBox.topY)) - + 12; + + elk + .layout(graph) + .then((resultGraph) => { + for (const elkEl of resultGraph.children) { + const group = groupMap.get(elkEl.id); + for (const groupEl of group.elements) { + const originalDistancX = groupEl.x - group.boundingBox.topX; + const originalDistancY = groupEl.y - group.boundingBox.topY; + const groupElDistanceX = + elkEl.x + initTopX + originalDistancX - groupEl.x; + const groupElDistanceY = + elkEl.y + initTopY + originalDistancY - groupEl.y; + + groupEl.x = groupEl.x + groupElDistanceX; + groupEl.y = groupEl.y + groupElDistanceY; + + if (stickynotesMap.has(groupEl.id)) { + const stickynote = stickynotesMap.get(groupEl.id); + stickynote.x = stickynote.x + groupElDistanceX; + stickynote.y = stickynote.y + groupElDistanceY; + } + } + } + + ea.copyViewElementsToEAforEditing(selectedElements); + ea.addElementsToView(false, false); + + normalizeSelectedArrows(); + }) + .catch(console.error); +} + +function loadELK(doAfterLoaded) { + let script = document.createElement("script"); + script.onload = function () { + if (typeof ELK !== "undefined") { + doAfterLoaded(); + } + }; + script.src = + "https://cdn.jsdelivr.net/npm/elkjs@0.8.2/lib/elk.bundled.min.js"; + document.head.appendChild(script); +} + +/* + * Normalize Selected Arrows + */ + +function normalizeSelectedArrows() { + let gapValue = 2; + + const selectedIndividualArrows = ea + .getMaximumGroups(ea.getViewSelectedElements()) + .reduce( + (result, group) => + group.length === 1 && + (group[0].type === "arrow" || group[0].type === "line") + ? [...result, group[0]] + : result, + [] + ); + + const allElements = ea.getViewElements(); + for (const arrow of selectedIndividualArrows) { + const startBindingEl = allElements.filter( + (el) => el.id === (arrow.startBinding || {}).elementId + )[0]; + const endBindingEl = allElements.filter( + (el) => el.id === (arrow.endBinding || {}).elementId + )[0]; + + if (startBindingEl) { + recalculateStartPointOfLine( + arrow, + startBindingEl, + endBindingEl, + gapValue + ); + } + if (endBindingEl) { + recalculateEndPointOfLine(arrow, endBindingEl, startBindingEl, gapValue); + } + } + + ea.copyViewElementsToEAforEditing(selectedIndividualArrows); + ea.addElementsToView(false, false); +} + +function recalculateStartPointOfLine(line, el, elB, gapValue) { + const aX = el.x + el.width / 2; + const bX = + line.points.length <= 2 && elB + ? elB.x + elB.width / 2 + : line.x + line.points[1][0]; + const aY = el.y + el.height / 2; + const bY = + line.points.length <= 2 && elB + ? elB.y + elB.height / 2 + : line.y + line.points[1][1]; + + line.startBinding.gap = gapValue; + line.startBinding.focus = 0; + const intersectA = ea.intersectElementWithLine( + el, + [bX, bY], + [aX, aY], + line.startBinding.gap + ); + + if (intersectA.length > 0) { + line.points[0] = [0, 0]; + for (let i = 1; i < line.points.length; i++) { + line.points[i][0] -= intersectA[0][0] - line.x; + line.points[i][1] -= intersectA[0][1] - line.y; + } + line.x = intersectA[0][0]; + line.y = intersectA[0][1]; + } +} + +function recalculateEndPointOfLine(line, el, elB, gapValue) { + const aX = el.x + el.width / 2; + const bX = + line.points.length <= 2 && elB + ? elB.x + elB.width / 2 + : line.x + line.points[line.points.length - 2][0]; + const aY = el.y + el.height / 2; + const bY = + line.points.length <= 2 && elB + ? elB.y + elB.height / 2 + : line.y + line.points[line.points.length - 2][1]; + + line.endBinding.gap = gapValue; + line.endBinding.focus = 0; + const intersectA = ea.intersectElementWithLine( + el, + [bX, bY], + [aX, aY], + line.endBinding.gap + ); + + if (intersectA.length > 0) { + line.points[line.points.length - 1] = [ + intersectA[0][0] - line.x, + intersectA[0][1] - line.y, + ]; + } +} \ No newline at end of file diff --git a/ea-scripts/Auto Layout.svg b/ea-scripts/Auto Layout.svg new file mode 100644 index 0000000..06f1022 --- /dev/null +++ b/ea-scripts/Auto Layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ea-scripts/index-new.md b/ea-scripts/index-new.md index 78e650f..fd0e53f 100644 --- a/ea-scripts/index-new.md +++ b/ea-scripts/index-new.md @@ -31,6 +31,7 @@ I would love to include your contribution in the script library. If you have a s |
|[[#Add Link to Existing File and Open]]| |
|[[#Add Link to New Page and Open]]| |
|[[#Add Next Step in Process]]| +|
|[[#Auto Layout]]| |
|[[#Box Each Selected Groups]]| |
|[[#Box Selected Elements]]| |
|[[#Change shape of selected elements]]| @@ -102,6 +103,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea ```
Author@zsviczian
SourceFile on GitHub
DescriptionThis script will prompt you for the title of the process step, then will create a stick note with the text. If an element is selected then the script will connect this new step with an arrow to the previous step (the selected element). If no element is selected, then the script assumes this is the first step in the process and will only output the sticky note with the text that was entered.
+## Auto Layout +```excalidraw-script-install +https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Layout.md +``` +
Author@1-2-3
SourceFile on GitHub
DescriptionThis script performs automatic layout for the selected top-level grouping objects. It is powered by elkjs and needs to be connected to the Internet.
+ ## Box Each Selected Groups ```excalidraw-script-install https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.md diff --git a/images/scripts-auto-layout.png b/images/scripts-auto-layout.png new file mode 100644 index 0000000..08eeb0e Binary files /dev/null and b/images/scripts-auto-layout.png differ