Skip to content

Commit

Permalink
Reorient view during relayout based on visible elements (#163)
Browse files Browse the repository at this point in the history
Relayout now tries to keep track of the visible elements from before relayouting. This should allow more fluid transition when collapsing elements. The heuristic condition is: every node that was entirely visible before the relayouting should still be visible after it. If they are all already visible, no changes are made to the view (we would like to minimize involuntary view changes as much as possible).
Additionally, if a specific element instigated the change (i.e., a node was collapsed), it will also be included in the nodes to view.

The PR also fixes a visual artifact introduced wherein collapsed nodes still appear for a few frames with their old coordinates, which results in a badly-drawn graph.
  • Loading branch information
tbennun authored Sep 3, 2024
1 parent 21a11bb commit ca8608e
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 34 deletions.
4 changes: 2 additions & 2 deletions src/renderer/canvas_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,10 @@ export class CanvasManager {
if (parent_graph && !(el instanceof Edge)) {
// Find all the edges connected to the moving node
parent_graph.outEdges(el.id.toString())?.forEach(edge_id => {
out_edges.push(parent_graph.edge(edge_id));
out_edges.push(parent_graph!.edge(edge_id));
});
parent_graph.inEdges(el.id.toString())?.forEach(edge_id => {
in_edges.push(parent_graph.edge(edge_id));
in_edges.push(parent_graph!.edge(edge_id));
});
}

Expand Down
142 changes: 113 additions & 29 deletions src/renderer/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ declare const canvas2pdf: any;
declare const vscode: any | null;

export type SDFGElementGroup = ('states' | 'nodes' | 'edges' | 'isedges' |
'connectors' | 'controlFlowRegions' |
'controlFlowBlocks');
'connectors' | 'controlFlowRegions' |
'controlFlowBlocks');
export interface SDFGElementInfo {
sdfg: JsonSDFG,
id: number,
Expand Down Expand Up @@ -121,6 +121,13 @@ export type CFGListType = {
}
};

export type VisibleElementsType = {
type: string,
stateId: number,
cfgId: number,
id: number,
}[];

function check_valid_add_position(
type: SDFGElementType | null,
foreground_elem: SDFGElement | undefined | null, lib: any, _mousepos: any
Expand Down Expand Up @@ -697,7 +704,7 @@ export class SDFGRenderer extends EventEmitter {
add_btn.onclick = () => {
this.mouse_mode = 'add';
this.add_type =
<SDFGElementType> add_btn.getAttribute('type');
<SDFGElementType>add_btn.getAttribute('type');
this.add_mode_lib = null;
this.add_edge_start = null;
this.add_edge_start_conn = null;
Expand Down Expand Up @@ -1242,10 +1249,15 @@ export class SDFGRenderer extends EventEmitter {
}

// Re-layout graph and nested graphs
public relayout(): DagreGraph {
public relayout(instigator: SDFGElement | null = null): DagreGraph {
if (!this.ctx)
throw new Error('No context found while performing layouting');

// Collect currently-visible elements for reorientation
const elements = this.getVisibleElementsAsObjects(true);
if (instigator)
elements.push(instigator);

for (const cfgId in this.cfgList) {
this.cfgList[cfgId].graph = null;
this.cfgList[cfgId].nsdfgNode = null;
Expand All @@ -1259,6 +1271,10 @@ export class SDFGRenderer extends EventEmitter {
for (const bId of this.graph.nodes())
topLevelBlocks.push(this.graph.node(bId));
this.graphBoundingBox = boundingBox(topLevelBlocks);

// Reorient view based on an approximate set of visible elements
this.reorient(elements);

this.onresize();

this.update_fast_memlet_lookup();
Expand Down Expand Up @@ -1287,6 +1303,41 @@ export class SDFGRenderer extends EventEmitter {
return this.graph;
}

public reorient(old_visible_elements: SDFGElement[]): void {
// Reorient view based on an approximate set of visible elements

// Nothing to reorient to
if (!old_visible_elements || old_visible_elements.length === 0)
return;

// If the current view contains everything that was visible before,
// no need to change anything.
const new_visible_elements = this.getVisibleElementsAsObjects(true);
const old_nodes = old_visible_elements.filter(x => (
x instanceof ControlFlowBlock ||
x instanceof SDFGNode));
const new_nodes = new_visible_elements.filter(x => (
x instanceof ControlFlowBlock ||
x instanceof SDFGNode));
const old_set = new Set(old_nodes.map(x => x.guid()));
const new_set = new Set(new_nodes.map(x => x.guid()));
const diff = old_set.difference(new_set);
if (diff.size === 0)
return;

// Reorient based on old visible elements refreshed to new locations
const old_elements_in_new_layout: SDFGElement[] = [];
this.doForAllGraphElements((group: SDFGElementGroup,
info: GraphElementInfo, elem: SDFGElement) => {
if (elem instanceof ControlFlowBlock || elem instanceof SDFGNode) {
const guid = elem.guid();
if (guid && old_set.has(guid))
old_elements_in_new_layout.push(elem);
}
});
this.zoom_to_view(old_elements_in_new_layout, true, undefined, false);
}

public translateMovedElements(): void {
if (!this.graph)
return;
Expand Down Expand Up @@ -1378,7 +1429,8 @@ export class SDFGRenderer extends EventEmitter {
// Change translation and scale such that the chosen elements
// (or entire graph if null) is in view
public zoom_to_view(
elements: any = null, animate: boolean = true, padding?: number
elements: any = null, animate: boolean = true, padding?: number,
redraw: boolean = true
): void {
if (!elements || elements.length === 0) {
elements = this.graph?.nodes().map(x => this.graph?.node(x));
Expand All @@ -1400,7 +1452,8 @@ export class SDFGRenderer extends EventEmitter {
const bb = boundingBox(elements, paddingAbs);
this.canvas_manager?.set_view(bb, animate);

this.draw_async();
if (redraw)
this.draw_async();
}

public zoomToFitWidth(): void {
Expand Down Expand Up @@ -1526,7 +1579,7 @@ export class SDFGRenderer extends EventEmitter {

traverseSDFGScopes(
this.graph, (node: SDFGNode, _: DagreGraph) => {
if(node.attributes().is_collapsed) {
if (node.attributes().is_collapsed) {
node.attributes().is_collapsed = false;
return false;
}
Expand Down Expand Up @@ -2047,12 +2100,7 @@ export class SDFGRenderer extends EventEmitter {
this.draw_async();
}

public getVisibleElements(): {
type: string,
stateId: number,
cfgId: number,
id: number,
}[] {
public getVisibleElements(): VisibleElementsType {
if (!this.canvas_manager)
return [];

Expand Down Expand Up @@ -2098,6 +2146,37 @@ export class SDFGRenderer extends EventEmitter {
return elements;
}

public getVisibleElementsAsObjects(
entirely_visible: boolean
): SDFGElement[] {
if (!this.canvas_manager)
return [];

const curx = this.canvas_manager.mapPixelToCoordsX(0);
const cury = this.canvas_manager.mapPixelToCoordsY(0);
const canvasw = this.canvas?.width;
const canvash = this.canvas?.height;
let endx = null;
if (canvasw)
endx = this.canvas_manager.mapPixelToCoordsX(canvasw);
let endy = null;
if (canvash)
endy = this.canvas_manager.mapPixelToCoordsY(canvash);
const curw = (endx ? endx : 0) - curx;
const curh = (endy ? endy : 0) - cury;
const elements: any[] = [];
this.doForIntersectedElements(
curx, cury, curw, curh,
(group, objInfo, _obj) => {
if (entirely_visible &&
!_obj.contained_in(curx, cury, curw, curh))
return;
elements.push(_obj);
}
);
return elements;
}

public doForVisibleElements(func: GraphElemFunction): void {
if (!this.canvas_manager)
return;
Expand Down Expand Up @@ -2675,7 +2754,7 @@ export class SDFGRenderer extends EventEmitter {
}
} else if (e instanceof InterstateEdge) {
if (!e.parentElem ||
(e.parentElem && e.parentElem instanceof SDFG)) {
(e.parentElem && e.parentElem instanceof SDFG)) {
e.sdfg.edges = e.sdfg.edges.filter(
(_, ind: number) => ind !== e.id
);
Expand Down Expand Up @@ -2770,7 +2849,12 @@ export class SDFGRenderer extends EventEmitter {

// Toggles collapsed state of foreground_elem if applicable.
// Returns true if re-layout occured and re-draw is necessary.
public toggle_element_collapse(foreground_elem: any): boolean {
public toggle_element_collapse(
foreground_elem: SDFGElement | null
): boolean {
if (!foreground_elem)
return false;

const sdfg = (foreground_elem ? foreground_elem.sdfg : null);
let sdfg_elem = null;
if (foreground_elem instanceof State) {
Expand All @@ -2783,29 +2867,29 @@ export class SDFGRenderer extends EventEmitter {
// If a scope exit node, use entry instead
if (sdfg_elem.type.endsWith('Exit') &&
foreground_elem.parent_id !== null) {
sdfg_elem = sdfg.nodes[foreground_elem.parent_id].nodes[
sdfg_elem.scope_entry
];
const parent = sdfg!.nodes[foreground_elem.parent_id];
if (parent.nodes)
sdfg_elem = parent.nodes[sdfg_elem.scope_entry];
}
} else {
sdfg_elem = null;
}

// Toggle collapsed state
if (foreground_elem.COLLAPSIBLE) {
if ('is_collapsed' in sdfg_elem.attributes) {
sdfg_elem.attributes.is_collapsed =
!sdfg_elem.attributes.is_collapsed;
} else {
sdfg_elem.attributes['is_collapsed'] = true;
}

this.emit('collapse_state_changed');

// Re-layout SDFG
this.add_loading_animation();
setTimeout(() => {
this.relayout();
if ('is_collapsed' in sdfg_elem.attributes) {
sdfg_elem.attributes.is_collapsed =
!sdfg_elem.attributes.is_collapsed;
} else {
sdfg_elem.attributes['is_collapsed'] = true;
}

this.relayout(foreground_elem);
this.draw_async();
}, 10);

Expand Down Expand Up @@ -3402,7 +3486,7 @@ export class SDFGRenderer extends EventEmitter {
if (obj.hovered && hover_changed &&
obj instanceof SDFGNode &&
(obj.in_summary_has_effect ||
obj.out_summary_has_effect)) {
obj.out_summary_has_effect)) {
// Setting these to false will cause the summary
// symbol not to be drawn in renderer_elements.ts
obj.summarize_in_edges = false;
Expand Down Expand Up @@ -3833,7 +3917,7 @@ export class SDFGRenderer extends EventEmitter {
return this.cfgList;
}

public getCFGTree(): { [key: number]: number} {
public getCFGTree(): { [key: number]: number } {
return this.cfgTree;
}

Expand Down Expand Up @@ -4462,7 +4546,7 @@ function relayoutSDFGState(
// If it's a nested SDFG, we need to record the node as all of its
// state's parent node.
if ((node.type === SDFGElementType.NestedSDFG ||
node.type === SDFGElementType.ExternalNestedSDFG) &&
node.type === SDFGElementType.ExternalNestedSDFG) &&
node.attributes.sdfg && node.attributes.sdfg.type !== 'SDFGShell') {
stateParentList[node.attributes.sdfg.cfg_list_id] = obj;
sdfgList[node.attributes.sdfg.cfg_list_id].nsdfgNode = obj;
Expand Down
16 changes: 13 additions & 3 deletions src/renderer/renderer_elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ export class SDFGElement {
return this.data.label;
}

public guid(): string {
// If GUID does not exist, fall back to element ID
return this.cfg?.cfg_list_id + '/' + (
this.parent_id ?? -1) + '/' + this.id;
}

// Text used for matching the element during a search
public text_for_find(): string {
return this.label();
Expand Down Expand Up @@ -1399,8 +1405,8 @@ export abstract class Edge extends SDFGElement {
};

// Check if the two rectangles intersect
if (r.x + r.w >= x && r.x <= x+w &&
r.y + r.h >= y && r.y <= y+h)
if (r.x + r.w >= x && r.x <= x + w &&
r.y + r.h >= y && r.y <= y + h)
return true;
}
return false;
Expand Down Expand Up @@ -1811,6 +1817,10 @@ export class Connector extends SDFGElement {
public linkedElem?: SDFGElement;
public connectorType: 'in' | 'out' = 'in';

public guid(): string {
return ''; // Connectors have no GUID
}

public draw(
renderer: SDFGRenderer, ctx: CanvasRenderingContext2D,
_mousepos?: Point2D, edge: Edge | null = null
Expand Down Expand Up @@ -3350,7 +3360,7 @@ export function drawOctagon(
export function drawEllipse(
ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number
): void {
ctx.ellipse(x+w/2, y+h/2, w/2, h/2, 0, 0, 2 * Math.PI);
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
}

export function drawTrapezoid(
Expand Down

0 comments on commit ca8608e

Please sign in to comment.