Skip to content

Commit

Permalink
#2022 Arrange Horizontal does not work when we have a detached link (#…
Browse files Browse the repository at this point in the history
…2048)

Signed-off-by: CTomlyn <[email protected]>
  • Loading branch information
tomlyn authored Jul 2, 2024
1 parent 07e6fd5 commit 2231f3e
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export default class ArrangeLayoutAction extends Action {
this.apiPipeline = this.objectModel.getAPIPipeline();

// Copy the nodes to remember their original positions.
this.existingNodes = this.apiPipeline.getNodes().map((n) => Object.assign({}, n));
this.existingNodes = this.apiPipeline.getNodes().map((n) => ({ ...n }));

// Copy the links (including detached links) to remember their original positions.
this.existingLinks = this.apiPipeline.getLinks().map((n) => ({ ...n }));
}

// Standard methods
Expand All @@ -35,6 +38,7 @@ export default class ArrangeLayoutAction extends Action {

undo() {
this.apiPipeline.replaceNodes(this.existingNodes);
this.apiPipeline.updateLinks(this.existingLinks);
}

redo() {
Expand Down
88 changes: 78 additions & 10 deletions canvas_modules/common-canvas/src/object-model/api-pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -843,37 +843,66 @@ export default class APIPipeline {
autoLayout(layoutDirection) {
const canvasInfoPipeline = this.objectModel.getCanvasInfoPipeline(this.pipelineId);
let movedNodesInfo = {};
let movedLinksInfo = {};
if (layoutDirection === VERTICAL) {
movedNodesInfo = this.dagreAutolayout(DAGRE_VERTICAL, canvasInfoPipeline);
[movedNodesInfo, movedLinksInfo] = this.dagreAutolayout(DAGRE_VERTICAL, canvasInfoPipeline);
} else {
movedNodesInfo = this.dagreAutolayout(DAGRE_HORIZONTAL, canvasInfoPipeline);
[movedNodesInfo, movedLinksInfo] = this.dagreAutolayout(DAGRE_HORIZONTAL, canvasInfoPipeline);
}

this.sizeAndPositionObjects(movedNodesInfo);
this.sizeAndPositionObjects(movedNodesInfo, movedLinksInfo);
}

// Returns two arrays containing info to indicate new auto-layout
// positions for nodes and, if present, fully-detached and semi-detached
// links. Uses the Dagre library to calculate the new positions.
dagreAutolayout(direction, canvasInfoPipeline) {
const canvasLayout = this.objectModel.getCanvasLayout();

var nodeLinks = canvasInfoPipeline.links.filter((link) => {
return link.type === NODE_LINK || link.type === ASSOCIATION_LINK;
});

let nodesData = [];

// Calculate some default dimensions for nodes.
const layout = this.objectModel.getCanvasConfig()?.enableNodeLayout;
const defaultWidth = layout?.defaultNodeWidth ? layout.defaultNodeWidth : 150;
const defaultHeight = layout?.defaultNodeHeight ? layout.defaultNodeHeight : 80;

// Create an array of edges, from the node links, to be passed to Dagre.
// At the same time, for any semi-detached or fully-detached links we
// create a temporary node so Dagre will also autlayout the ends of
// detached links.
var edges = nodeLinks.map((link) => {
return { "v": link.srcNodeId, "w": link.trgNodeId, "value": { "points": [] } };
let srcNodeId = link.srcNodeId;
let trgNodeId = link.trgNodeId;

if (link.srcPos) {
srcNodeId = "temp-src-node-" + link.id;
nodesData.push({ "v": srcNodeId, "value": { width: defaultWidth, height: defaultHeight } });
}

if (link.trgPos) {
trgNodeId = "temp-trg-node-" + link.id;
nodesData.push({ "v": trgNodeId, "value": { width: defaultWidth, height: defaultHeight } });
}
return { "v": srcNodeId, "w": trgNodeId, "value": { "points": [] } };
});

var nodesData = canvasInfoPipeline.nodes.map((node) => {
// Add actual nodes to nodesData and adjust width if necessary
nodesData = nodesData.concat(canvasInfoPipeline.nodes.map((node) => {
let newWidth = node.width;
if (direction === DAGRE_HORIZONTAL) {
const padding = this.getPaddingForNode(node, canvasLayout, canvasInfoPipeline);
newWidth = node.width +
Math.max(this.getPaddingForNode(node, canvasLayout, canvasInfoPipeline), canvasLayout.autoLayoutHorizontalSpacing);
Math.max(padding, canvasLayout.autoLayoutHorizontalSpacing);
}

return { "v": node.id, "value": { width: newWidth, height: node.height } };
});
}));

// possible values: TB, BT, LR, or RL, where T = top, B = bottom, L = left, and R = right.
// Possible values: TB, BT, LR, or RL, where T = top, B = bottom, L = left, and R = right.
// default TB for vertical layout
// set to LR for horizontal layout
var value = { };
Expand Down Expand Up @@ -906,15 +935,16 @@ export default class APIPipeline {

const outputGraph = dagre.graphlib.json.write(g);
const movedNodesInfo = this.convertGraphToMovedNodes(outputGraph, canvasInfoPipeline.nodes);
const movedLinksInfo = this.convertGraphToMovedLinks(outputGraph);

return movedNodesInfo;
return [movedNodesInfo, movedLinksInfo];
}

// Returns an array of move node actions that can be used to reposition the
// nodes based on the provided Dagre output graph. (The node width and height
// are included in the output because the move nodes action expects them).
convertGraphToMovedNodes(outputGraph, canvasInfoPipelineNodes) {
const movedNodesInfo = [];
const movedNodesInfo = {};
const lookup = {};

for (var i = 0, len = outputGraph.nodes.length; i < len; i++) {
Expand All @@ -936,6 +966,44 @@ export default class APIPipeline {
return movedNodesInfo;
}

// Returns an array of move link actions that can be used to reposition the
// links based on the provided Dagre output graph.
// Iterate nodes and check if there are any temporary nodes and if so,
// use the x/y coordinates of those nodes as the start/end of the
// corresponding detached link.
convertGraphToMovedLinks(outputGraph) {
const movedLinksInfo = [];

const outNodes = outputGraph.nodes;

// Record the x and y position of temporary node to be assigned to the detached link.
// A fully detached link can have both temp-src-node and temp-trg-node
outNodes.forEach((node) => {
if (node.v.startsWith("temp-src-node-")) {
const linkId = node.v.split("temp-src-node-")[1];

movedLinksInfo[linkId] = {
srcPos: {
x_pos: node.value.x - (node.value.width / 2),
y_pos: node.value.y
}
};
}
if (node.v.startsWith("temp-trg-node-")) {
const linkId = node.v.split("temp-trg-node-")[1];

movedLinksInfo[linkId] = {
// If it is a fully detached link include srcPos cordinates.
...movedLinksInfo[linkId],
trgPos: {
x_pos: node.value.x - (node.value.width / 2),
y_pos: node.value.y
}
};
}
});
return movedLinksInfo;
}

// Returns a width that can be added to the node width for auto-layout.
// This extra width is what is needed to display the connection lines
Expand Down
102 changes: 102 additions & 0 deletions canvas_modules/harness/cypress/e2e/canvas/auto-layout-dagre.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,108 @@ describe("Test for toolbar horizontal and vertical layout", function() {
});
});

describe("Test for toolbar horizontal and vertical layout for a detached link", function() {
beforeEach(() => {
cy.visit("/");
cy.setCanvasConfig({ "selectedToolbarType": "SingleLeftBarArray",
"selectedLinkSelection": "Detachable" });
cy.openCanvasDefinition("detachedLinksCanvas.json");
});

it("Test horizontal and vertical autolayout of detached links", function() {
// Create a function to verify the initial layout.
const verifyInitialLayout = () => {
cy.verifyNumberOfLinks(13);
cy.verifyNumberOfNodes(6);

// Verify the original positions of the three semi-detached links.
cy.verifyDetachedLinkPathFromSource("Binding (entry) node", "outPort", 1, [
"M 140 194 Q 220 194 220 350"
]);

cy.verifyDetachedLinkPathToTarget("Execution node", "inPort", 1, [
"M 260 360 Q 260 194 332.5 194"
]);

cy.verifyDetachedLinkPathFromSource("Binding (exit) node", "outPort", 1, [
"M 612.5 419 C 674.2556762695312 419 674.2556762695312 " +
"381.00567626953125 736.0113525390625 381.00567626953125"
]);
};

// Create a function to verify the horizontal layout.
const verifyHorizontalAutoLayout = () => {
cy.verifyNumberOfLinks(13);
cy.verifyNumberOfNodes(6);

// Verify the horizontal positions of the three semi-detached links.
cy.verifyDetachedLinkPathFromSource("Binding (entry) node", "outPort", 1, [
"M 120 234 C 180 234 180 242.5 240 242.5"
]);

cy.verifyDetachedLinkPathToTarget("Execution node", "inPort", 1, [
"M 90 397.5 C 145 397.5 145 389 200 389"
]);

cy.verifyDetachedLinkPathFromSource("Binding (exit) node", "outPort", 1, [
"M 570 466.5 C 630 466.5 630 562.5 690 562.5"
]);
};

// Create a function to verify the vertical layout.
const verifyVerticalAutoLayout = () => {
cy.verifyNumberOfLinks(13);
cy.verifyNumberOfNodes(6);

// Verify the vertical positions of the three semi-detached links.
cy.verifyDetachedLinkPathFromSource("Binding (entry) node", "outPort", 1, [
"M 270 79 Q 300 79 300 145.75 Q 300 212.5 250 212.5 L 250 212.5 Q 200 212.5 200 242.5"
]);

cy.verifyDetachedLinkPathToTarget("Execution node", "inPort", 1, [
"M 350 87.5 Q 350 117.5 335 117.5 Q 320 117.5 320 175.75 L 320 175.75 Q 320 234 350 234"
]);

cy.verifyDetachedLinkPathFromSource("Binding (exit) node", "outPort", 1, [
"M 495 544 Q 525 544 525 610.75 Q 525 677.5 517.5 677.5 L 517.5 677.5 Q 510 677.5 510 707.5"
]);
};

// Make sure the initial layout is correct.
verifyInitialLayout();

// Do vertical autolayout and verify layout
cy.clickToolbarArrangeHorizontally();
cy.clickToolbarZoomToFit();
verifyHorizontalAutoLayout();

// Undo and verify layout
cy.clickToolbarUndo();
verifyInitialLayout();

// Redo and verify layout
cy.clickToolbarRedo();
verifyHorizontalAutoLayout();

// Undo to get back to original layout
cy.clickToolbarUndo();
verifyInitialLayout();

// Do vertical autolayout and verify layout
cy.clickToolbarArrangeVertically();
cy.clickToolbarZoomToFit();
verifyVerticalAutoLayout();

// Undo and verify layout
cy.clickToolbarUndo();
verifyInitialLayout();

// Redo and verify layout
cy.clickToolbarRedo();
verifyVerticalAutoLayout();
});
});

describe("Test the horizontal layout of a multiport node with many descendants", function() {
beforeEach(() => {
cy.visit("/");
Expand Down

0 comments on commit 2231f3e

Please sign in to comment.