From 6ff918f166cbd595ce2483281e2ef4994bb628e7 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Sat, 31 Aug 2024 17:08:24 -0700 Subject: [PATCH 1/6] Reorient view during relayout based on visible elements Relayout now tries to keep track of the visible elements from before relayouting. This should allow more fluid transition when collapsing elements. --- src/renderer/renderer.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 0bba060d..37e4c685 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -1246,6 +1246,9 @@ export class SDFGRenderer extends EventEmitter { if (!this.ctx) throw new Error('No context found while performing layouting'); + // Collect currently-visible elements for reorientation + let elements = this.getVisibleElements(); + for (const cfgId in this.cfgList) { this.cfgList[cfgId].graph = null; this.cfgList[cfgId].nsdfgNode = null; @@ -1259,6 +1262,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.zoom_to_view(elements, false, 0, false); + this.onresize(); this.update_fast_memlet_lookup(); @@ -1378,7 +1385,7 @@ 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 = false ): void { if (!elements || elements.length === 0) { elements = this.graph?.nodes().map(x => this.graph?.node(x)); @@ -1400,7 +1407,9 @@ 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 { From c1dcfa3e296c086f8d3f9d54b73aa186ed28f6e4 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Sat, 31 Aug 2024 19:41:09 -0700 Subject: [PATCH 2/6] minor type fixes --- src/renderer/canvas_manager.ts | 4 +-- src/renderer/renderer.ts | 49 ++++++++++++++++++------------- src/renderer/renderer_elements.ts | 12 ++++---- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/renderer/canvas_manager.ts b/src/renderer/canvas_manager.ts index fb7717a3..7823e567 100644 --- a/src/renderer/canvas_manager.ts +++ b/src/renderer/canvas_manager.ts @@ -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)); }); } diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 37e4c685..e929990b 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -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, @@ -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 @@ -697,7 +704,7 @@ export class SDFGRenderer extends EventEmitter { add_btn.onclick = () => { this.mouse_mode = 'add'; this.add_type = - add_btn.getAttribute('type'); + add_btn.getAttribute('type'); this.add_mode_lib = null; this.add_edge_start = null; this.add_edge_start_conn = null; @@ -1385,7 +1392,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, redraw: boolean = false + 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)); @@ -1407,9 +1415,8 @@ export class SDFGRenderer extends EventEmitter { const bb = boundingBox(elements, paddingAbs); this.canvas_manager?.set_view(bb, animate); - if (redraw) { + if (redraw) this.draw_async(); - } } public zoomToFitWidth(): void { @@ -1535,7 +1542,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; } @@ -2056,12 +2063,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 []; @@ -2684,7 +2686,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 ); @@ -2779,7 +2781,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) { @@ -2792,9 +2799,9 @@ 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; @@ -3411,7 +3418,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; @@ -3842,7 +3849,7 @@ export class SDFGRenderer extends EventEmitter { return this.cfgList; } - public getCFGTree(): { [key: number]: number} { + public getCFGTree(): { [key: number]: number } { return this.cfgTree; } @@ -4471,7 +4478,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; diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index 7c99d901..6350cb40 100644 --- a/src/renderer/renderer_elements.ts +++ b/src/renderer/renderer_elements.ts @@ -1399,8 +1399,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; @@ -2115,9 +2115,9 @@ export class ScopeNode extends SDFGNode { SDFV.DEFAULT_MAX_FONTSIZE, 0.7, SDFV.DEFAULT_FAR_FONT_MULTIPLIER, true, TextVAlign.BOTTOM, TextHAlign.RIGHT, { - bottom: 2.0, - right: this.height, - } + bottom: 2.0, + right: this.height, + } ); } } @@ -3350,7 +3350,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( From 03b80202eb56005509c316f61f2953c7a3f9f423 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Sat, 31 Aug 2024 19:41:47 -0700 Subject: [PATCH 3/6] Add (backwards and forwards compatible) GUID function to SDFG element --- src/renderer/renderer_elements.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index 6350cb40..e35bf44a 100644 --- a/src/renderer/renderer_elements.ts +++ b/src/renderer/renderer_elements.ts @@ -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(); @@ -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 From cd4b793b7e03d9123f3efa9763e54a6bcdba6f70 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Sat, 31 Aug 2024 19:42:56 -0700 Subject: [PATCH 4/6] Reorient based on fully-visible nodes only, and according to their new positions on the relayouted graph --- src/renderer/renderer.ts | 76 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index e929990b..ab25e9f9 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -1249,12 +1249,14 @@ 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 - let elements = this.getVisibleElements(); + const elements = this.getVisibleElementsAsObjects(true); + if (instigator) + elements.push(instigator); for (const cfgId in this.cfgList) { this.cfgList[cfgId].graph = null; @@ -1271,7 +1273,7 @@ export class SDFGRenderer extends EventEmitter { this.graphBoundingBox = boundingBox(topLevelBlocks); // Reorient view based on an approximate set of visible elements - this.zoom_to_view(elements, false, 0, false); + this.reorient(elements); this.onresize(); @@ -1301,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; @@ -2109,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; @@ -2821,7 +2889,7 @@ export class SDFGRenderer extends EventEmitter { // Re-layout SDFG this.add_loading_animation(); setTimeout(() => { - this.relayout(); + this.relayout(foreground_elem); this.draw_async(); }, 10); From f3160cf1f812b35a36a1634111400b0da1fc3ae4 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Sat, 31 Aug 2024 19:43:25 -0700 Subject: [PATCH 5/6] Fix long-standing visual artifact in which collapsing shows a badly-rendered node --- src/renderer/renderer.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index ab25e9f9..b8db6676 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -2877,18 +2877,18 @@ export class SDFGRenderer extends EventEmitter { // 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(() => { + 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); From 2b9863129d116c2b269195b5ed4939347776ed73 Mon Sep 17 00:00:00 2001 From: Philipp Schaad Date: Tue, 3 Sep 2024 10:22:03 +0200 Subject: [PATCH 6/6] Auto-formatter snafu --- src/renderer/renderer_elements.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index e35bf44a..cca8afa8 100644 --- a/src/renderer/renderer_elements.ts +++ b/src/renderer/renderer_elements.ts @@ -2125,9 +2125,9 @@ export class ScopeNode extends SDFGNode { SDFV.DEFAULT_MAX_FONTSIZE, 0.7, SDFV.DEFAULT_FAR_FONT_MULTIPLIER, true, TextVAlign.BOTTOM, TextHAlign.RIGHT, { - bottom: 2.0, - right: this.height, - } + bottom: 2.0, + right: this.height, + } ); } }