Merge pull request #922 from 1-2-3/master

add auto layout ea-script
This commit is contained in:
zsviczian
2022-12-04 18:44:16 +01:00
committed by GitHub
4 changed files with 415 additions and 0 deletions

407
ea-scripts/Auto Layout.md Normal file
View File

@@ -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 <a href="http://www.eclipse.org/elk/reference.html" rel="nofollow">ELK's documentation</a>.`,
},
};
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,
];
}
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670131481615" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3504" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M947.2 0H76.8C33.6 0 0 33.6 0 76.8v870.4C0 990.4 33.6 1024 76.8 1024h870.4c38.4 0 72-30.4 76.8-68.8V76.8C1024 33.6 990.4 0 947.2 0zM84.8 84.8h852.8V256H84.8V84.8z m256 256h596.8v256H340.8v-256z m-256 598.4V340.8H256v596.8H84.8z m256 0v-256h596.8v256H340.8z" p-id="3505"></path></svg>

After

Width:  |  Height:  |  Size: 616 B

View File

@@ -31,6 +31,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Layout.svg"/></div>|[[#Auto Layout]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.svg"/></div>|[[#Box Each Selected Groups]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Selected%20Elements.svg"/></div>|[[#Box Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
@@ -102,6 +103,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Add%20Next%20Step%20in%20Process.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This 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.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-process-step.jpg'></td></tr></table>
## Auto Layout
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Layout.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Auto%20Layout.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script performs automatic layout for the selected top-level grouping objects. It is powered by <a href='https://github.com/kieler/elkjs'>elkjs</a> and needs to be connected to the Internet.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-auto-layout.png'></td></tr></table>
## Box Each Selected Groups
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB