399 lines
12 KiB
Markdown
399 lines
12 KiB
Markdown
/*
|
|
|
|

|
|
|
|
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.
|
|
|
|

|
|
|
|
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, g) => [...result, ...g.filter(el => el.type === 'arrow')], []);
|
|
|
|
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,
|
|
];
|
|
}
|
|
} |