From ca8608ef9dacdb72aa7dc672aaa4d4e471cc9da0 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Tue, 3 Sep 2024 07:40:08 -0700 Subject: [PATCH] Reorient view during relayout based on visible elements (#163) 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. --- src/renderer/canvas_manager.ts | 4 +- src/renderer/renderer.ts | 142 ++++++++++++++++++++++++------ src/renderer/renderer_elements.ts | 16 +++- 3 files changed, 128 insertions(+), 34 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 0bba060d..b8db6676 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; @@ -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; @@ -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(); @@ -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; @@ -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)); @@ -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 { @@ -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; } @@ -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 []; @@ -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; @@ -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 ); @@ -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) { @@ -2783,9 +2867,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; @@ -2793,19 +2877,19 @@ 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(() => { - 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); @@ -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; @@ -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; } @@ -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; diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index 7c99d901..cca8afa8 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(); @@ -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; @@ -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 @@ -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(