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(