From 8961a69ae3ff21a4acac85dfbae1d1aea916371d Mon Sep 17 00:00:00 2001 From: Philipp Schaad Date: Thu, 2 Nov 2023 11:01:23 +0100 Subject: [PATCH] October Improvements 2/2 (#119) * Adapt to change in naming * amend * Improve permanent interstate edge labels * Add proper multiline support --- package.json | 2 +- src/renderer/renderer.ts | 31 ++--- src/renderer/renderer_elements.ts | 185 ++++++++++++++++++++++-------- src/utils/sdfg/sdfg_parser.ts | 2 +- 4 files changed, 158 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index c9405b88..f313e3ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@spcl/sdfv", - "version": "1.1.1", + "version": "1.1.2", "description": "A standalone viewer for SDFGs", "homepage": "https://github.com/spcl/dace-webclient", "main": "out/index.js", diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 2def3559..f6f6554e 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -47,13 +47,13 @@ import { deepCopy, intersectRect, showErrorModal } from '../utils/utils'; import { CanvasManager } from './canvas_manager'; import { AccessNode, Connector, - Edge, EntryNode, InterstateEdge, LoopScopeBlock, Memlet, NestedSDFG, + Edge, EntryNode, InterstateEdge, LoopRegion, Memlet, NestedSDFG, SDFG, SDFGElement, SDFGElementType, SDFGElements, SDFGNode, - ScopeBlock, + ControlFlowRegion, State, Tasklet, drawSDFG, @@ -2651,7 +2651,7 @@ export class SDFGRenderer extends EventEmitter { let sdfg_elem = null; if (foreground_elem instanceof State) sdfg_elem = foreground_elem.data.state; - else if (foreground_elem instanceof ScopeBlock) + else if (foreground_elem instanceof ControlFlowRegion) sdfg_elem = foreground_elem.data.block; else if (foreground_elem instanceof SDFGNode) { sdfg_elem = foreground_elem.data.node; @@ -3337,9 +3337,9 @@ function relayoutStateMachine( let blockGraph = null; if (block.attributes.is_collapsed) { blockInfo.height = SDFV.LINEHEIGHT; - if (blockElem instanceof LoopScopeBlock) { + if (blockElem instanceof LoopRegion) { const oldFont = ctx.font; - ctx.font = LoopScopeBlock.LOOP_STATEMENT_FONT; + ctx.font = LoopRegion.LOOP_STATEMENT_FONT; const labelWidths = [ ctx.measureText( (block.attributes.scope_condition?.string_data ?? '') + @@ -3358,7 +3358,7 @@ function relayoutStateMachine( ctx.font = oldFont; blockInfo.width = Math.max( maxLabelWidth, ctx.measureText(block.label).width - ) + 3 * LoopScopeBlock.META_LABEL_MARGIN; + ) + 3 * LoopRegion.META_LABEL_MARGIN; } else if (blockElem instanceof State) { blockInfo.width = ctx.measureText(blockInfo.label).width; } @@ -3373,16 +3373,16 @@ function relayoutStateMachine( blockInfo.width += 2 * BLOCK_MARGIN; blockInfo.height += 2 * BLOCK_MARGIN; - if (blockElem instanceof LoopScopeBlock) { + if (blockElem instanceof LoopRegion) { // Add spacing for the condition if the loop is not inverted. if (!block.attributes.inverted) - blockInfo.height += LoopScopeBlock.CONDITION_SPACING; + blockInfo.height += LoopRegion.CONDITION_SPACING; // If there's an init statement, add space for it. if (block.attributes.init_statement) - blockInfo.height += LoopScopeBlock.INIT_SPACING; + blockInfo.height += LoopRegion.INIT_SPACING; // If there's an update statement, also add space for it. if (block.attributes.update_statement) - blockInfo.height += LoopScopeBlock.UPDATE_SPACING; + blockInfo.height += LoopRegion.UPDATE_SPACING; } blockElem.data.layout = blockInfo; @@ -3394,7 +3394,8 @@ function relayoutStateMachine( for (let id = 0; id < stateMachine.edges.length; id++) { const edge = stateMachine.edges[id]; g.setEdge(edge.src, edge.dst, new InterstateEdge( - edge.attributes.data, id, sdfg + edge.attributes.data, id, sdfg, parent.id, parent, edge.src, + edge.dst )); } @@ -3454,13 +3455,13 @@ function relayoutStateMachine( // Base spacing for the inside. let topSpacing = BLOCK_MARGIN; - if (gBlock instanceof LoopScopeBlock) { + if (gBlock instanceof LoopRegion) { // Add spacing for the condition if the loop isn't inverted. if (!block.attributes.inverted) - topSpacing += LoopScopeBlock.CONDITION_SPACING; + topSpacing += LoopRegion.CONDITION_SPACING; // If there's an init statement, add space for it. if (block.attributes.init_statement) - topSpacing += LoopScopeBlock.INIT_SPACING; + topSpacing += LoopRegion.INIT_SPACING; } offset_sdfg(block as any, gBlock.data.graph, { x: topleft.x + BLOCK_MARGIN, @@ -3861,7 +3862,7 @@ function relayoutSDFGBlock( omitAccessNodes: boolean, parent: SDFGElement ): DagreSDFG | null { switch (block.type) { - case SDFGElementType.LoopScopeBlock: + case SDFGElementType.LoopRegion: return relayoutStateMachine( ctx, block as StateMachineType, sdfg, sdfgList, stateParentList, omitAccessNodes, parent diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index 0be4c07a..86b53025 100644 --- a/src/renderer/renderer_elements.ts +++ b/src/renderer/renderer_elements.ts @@ -39,8 +39,8 @@ export enum SDFGElementType { Reduce = 'Reduce', BasicBlock = 'BasicBlock', ControlFlowBlock = 'ControlFlowBlock', - ScopeBlock = 'ScopeBlock', - LoopScopeBlock = 'LoopScopeBlock', + ControlFlowRegion = 'ControlFlowRegion', + LoopRegion = 'LoopRegion', } export class SDFGElement { @@ -232,7 +232,7 @@ export class ControlFlowBlock extends SDFGElement { export class BasicBlock extends SDFGElement { } -export class ScopeBlock extends ControlFlowBlock { +export class ControlFlowRegion extends ControlFlowBlock { } export class State extends BasicBlock { @@ -385,7 +385,7 @@ export class State extends BasicBlock { } -export class LoopScopeBlock extends ScopeBlock { +export class LoopRegion extends ControlFlowRegion { public static readonly META_LABEL_MARGIN: number = 5; @@ -458,22 +458,22 @@ export class LoopScopeBlock extends ScopeBlock { ); const oldFont = ctx.font; - let topSpacing = LoopScopeBlock.META_LABEL_MARGIN; + let topSpacing = LoopRegion.META_LABEL_MARGIN; let remainingHeight = this.height; // Draw the init statement if there is one. if (this.attributes().init_statement) { - topSpacing += LoopScopeBlock.INIT_SPACING; - const initBottomLineY = topleft.y + LoopScopeBlock.INIT_SPACING; + topSpacing += LoopRegion.INIT_SPACING; + const initBottomLineY = topleft.y + LoopRegion.INIT_SPACING; ctx.beginPath(); ctx.moveTo(topleft.x, initBottomLineY); ctx.lineTo(topleft.x + this.width, initBottomLineY); ctx.stroke(); - ctx.font = LoopScopeBlock.LOOP_STATEMENT_FONT; + ctx.font = LoopRegion.LOOP_STATEMENT_FONT; const initStatement = this.attributes().init_statement.string_data; const initTextY = ( - (topleft.y + (LoopScopeBlock.INIT_SPACING / 2)) + + (topleft.y + (LoopRegion.INIT_SPACING / 2)) + (SDFV.LINEHEIGHT / 2) ); const initTextMetrics = ctx.measureText(initStatement); @@ -482,7 +482,7 @@ export class LoopScopeBlock extends ScopeBlock { ctx.font = oldFont; ctx.fillText( - 'init', topleft.x + LoopScopeBlock.META_LABEL_MARGIN, initTextY + 'init', topleft.x + LoopRegion.META_LABEL_MARGIN, initTextY ); } @@ -491,24 +491,24 @@ export class LoopScopeBlock extends ScopeBlock { // (do-while-style) loop). If the condition is drawn on top, make sure // the init statement spacing is respected if there is one. let condTopY = topleft.y; - let condLineY = condTopY + LoopScopeBlock.CONDITION_SPACING; + let condLineY = condTopY + LoopRegion.CONDITION_SPACING; if (this.attributes().inverted) { condTopY = topleft.y + - (this.height - LoopScopeBlock.CONDITION_SPACING); - condLineY = condTopY - LoopScopeBlock.CONDITION_SPACING; + (this.height - LoopRegion.CONDITION_SPACING); + condLineY = condTopY - LoopRegion.CONDITION_SPACING; } else if (this.attributes().init_statement) { - condTopY += LoopScopeBlock.INIT_SPACING; - condLineY = condTopY + LoopScopeBlock.CONDITION_SPACING; + condTopY += LoopRegion.INIT_SPACING; + condLineY = condTopY + LoopRegion.CONDITION_SPACING; } - topSpacing += LoopScopeBlock.CONDITION_SPACING; + topSpacing += LoopRegion.CONDITION_SPACING; ctx.beginPath(); ctx.moveTo(topleft.x, condLineY); ctx.lineTo(topleft.x + this.width, condLineY); ctx.stroke(); - ctx.font = LoopScopeBlock.LOOP_STATEMENT_FONT; - const condStatement = this.attributes().scope_condition.string_data; + ctx.font = LoopRegion.LOOP_STATEMENT_FONT; + const condStatement = this.attributes().loop_condition.string_data; const condTextY = ( - (condTopY + (LoopScopeBlock.CONDITION_SPACING / 2)) + + (condTopY + (LoopRegion.CONDITION_SPACING / 2)) + (SDFV.LINEHEIGHT / 2) ); const condTextMetrics = ctx.measureText(condStatement); @@ -516,25 +516,25 @@ export class LoopScopeBlock extends ScopeBlock { ctx.fillText(condStatement, condTextX, condTextY); ctx.font = oldFont; ctx.fillText( - 'while', topleft.x + LoopScopeBlock.META_LABEL_MARGIN, condTextY + 'while', topleft.x + LoopRegion.META_LABEL_MARGIN, condTextY ); // Draw the update statement if there is one. if (this.attributes().update_statement) { - remainingHeight -= LoopScopeBlock.UPDATE_SPACING; + remainingHeight -= LoopRegion.UPDATE_SPACING; const updateTopY = topleft.y + ( - this.height - LoopScopeBlock.UPDATE_SPACING + this.height - LoopRegion.UPDATE_SPACING ); ctx.beginPath(); ctx.moveTo(topleft.x, updateTopY); ctx.lineTo(topleft.x + this.width, updateTopY); ctx.stroke(); - ctx.font = LoopScopeBlock.LOOP_STATEMENT_FONT; + ctx.font = LoopRegion.LOOP_STATEMENT_FONT; const updateStatement = this.attributes().update_statement.string_data; const updateTextY = ( - (updateTopY + (LoopScopeBlock.UPDATE_SPACING / 2)) + + (updateTopY + (LoopRegion.UPDATE_SPACING / 2)) + (SDFV.LINEHEIGHT / 2) ); const updateTextMetrics = ctx.measureText(updateStatement); @@ -542,7 +542,7 @@ export class LoopScopeBlock extends ScopeBlock { ctx.fillText(updateStatement, updateTextX, updateTextY); ctx.font = oldFont; ctx.fillText( - 'update', topleft.x + LoopScopeBlock.META_LABEL_MARGIN, + 'update', topleft.x + LoopRegion.META_LABEL_MARGIN, updateTextY ); } @@ -554,7 +554,7 @@ export class LoopScopeBlock extends ScopeBlock { visibleRect.y <= topleft.y + SDFV.LINEHEIGHT && SDFVSettings.showStateNames) ctx.fillText( - this.label(), topleft.x + LoopScopeBlock.META_LABEL_MARGIN, + this.label(), topleft.x + LoopRegion.META_LABEL_MARGIN, topleft.y + topSpacing + SDFV.LINEHEIGHT ); @@ -1031,6 +1031,19 @@ export class Memlet extends Edge { export class InterstateEdge extends Edge { + // Parent ID is the state ID, if relevant + public constructor( + data: any, + id: number, + sdfg: JsonSDFG, + parent_id: number | null = null, + parentElem?: SDFGElement, + public readonly src?: string, + public readonly dst?: string, + ) { + super(data, id, sdfg, parent_id, parentElem); + } + public create_arrow_line(ctx: CanvasRenderingContext2D): void { // Draw intersate edges with bezier curves through the arrow points. ctx.moveTo(this.points[0].x, this.points[0].y); @@ -1116,12 +1129,11 @@ export class InterstateEdge extends Edge { this.points[this.points.length - 1], 3 ); - if (SDFVSettings.alwaysOnISEdgeLabels) { + if (SDFVSettings.alwaysOnISEdgeLabels) this.drawLabel(renderer, ctx); - } else { - if (this.hovered) - renderer.set_tooltip((c) => this.tooltip(c, renderer)); - } + + if (this.hovered) + renderer.set_tooltip((c) => this.tooltip(c, renderer)); } public tooltip(container: HTMLElement, renderer?: SDFGRenderer): void { @@ -1142,21 +1154,104 @@ export class InterstateEdge extends Edge { return; if ((ctx as any).lod && ppp >= SDFV.SCOPE_LOD) return; - ctx.fillStyle = this.getCssProperty( - renderer, '--interstate-edge-color' - ); + + const labelLines = []; + if (this.attributes().assignments) { + for (const k of Object.keys(this.attributes().assignments)) + labelLines.push(k + ' 🡐 ' + this.attributes().assignments[k]); + } + const cond = this.attributes().condition.string_data; + if (cond && cond !== '1' && cond !== 'true') + labelLines.push('if ' + cond); + + if (labelLines.length < 1) + return; + const oldFont = ctx.font; ctx.font = '8px sans-serif'; - const labelMetrics = ctx.measureText(this.label()); - const labelW = Math.abs(labelMetrics.actualBoundingBoxLeft) + - Math.abs(labelMetrics.actualBoundingBoxRight); - const labelH = Math.abs(labelMetrics.actualBoundingBoxDescent) + - Math.abs(labelMetrics.actualBoundingBoxAscent); - const offsetX = this.points[0].x > this.points[1].x ? -(labelW + 5) : 5; - const offsetY = this.points[0].y > this.points[1].y ? -5 : (labelH + 5); - ctx.fillText( - this.label(), this.points[0].x + offsetX, this.points[0].y + offsetY + const labelHs = []; + const labelWs = []; + for (const l of labelLines) { + const labelMetrics = ctx.measureText(l); + labelWs.push( + Math.abs(labelMetrics.actualBoundingBoxLeft) + + Math.abs(labelMetrics.actualBoundingBoxRight) + ); + labelHs.push( + Math.abs(labelMetrics.actualBoundingBoxDescent) + + Math.abs(labelMetrics.actualBoundingBoxAscent) + ); + } + const labelW = Math.max(...labelWs); + const labelH = labelHs.reduce((pv, cv) => { + if (!cv) + return pv; + return cv + SDFV.LINEHEIGHT + pv; + }); + + // The label is positioned at the origin of the interstate edge, offset + // so that it does not intersect the edge or the state it originates + // from. There are a few cases to consider: + // 1. The edge exits from the top/bottom of a node. Then the label is + // placed right next to the source point, offset up/down by + // LABEL_PADDING pixels to not intersect with the state. If the edge + // moves to the right/left, place the label to the left/right of the + // edge to avoid intersection. + // 2. The edge exits from the side of a node. Then the label is placed + // next to the source point, offset up/down by LABEL_PADDING pixels + // depending on whether the edge direction is down/up, so it does not + // intersect with the edge. To avoid intersecting with the node, the + // label is also offset LABEL_PADDING pixels to the left/right, + // depending on whether the edge exits to the left/right of the node. + const LABEL_PADDING = 3; + const srcP = this.points[0]; + const srcNode = this.src !== undefined ? + renderer.get_graph()?.node(this.src) : null; + // Initial offsets are good for edges coming out of a node's top border. + let offsetX = LABEL_PADDING; + let offsetY = -LABEL_PADDING; + if (srcNode) { + const stl = srcNode.topleft(); + if (Math.abs(srcP.y - (stl.y + srcNode.height)) < 1) { + // Edge exits the bottom of a node. + offsetY = LABEL_PADDING + labelH; + // If the edge moves right, offset the label to the left. + if (this.points[1].x > srcP.x) + offsetX = -(LABEL_PADDING + labelW); + } else if (Math.abs(srcP.x - stl.x) < 1) { + // Edge exits to the left of a node. + offsetX = -(LABEL_PADDING + labelW); + // If the edge moves down, offset the label upwards. + if (this.points[1].y <= srcP.y) + offsetY = LABEL_PADDING + labelH; + } else if (Math.abs(srcP.x - (stl.x + srcNode.width)) < 1) { + // Edge exits to the right of a node. + // If the edge moves down, offset the label upwards. + if (this.points[1].y <= srcP.y) + offsetY = LABEL_PADDING + labelH; + } else { + // Edge exits the top of a node. + // If the edge moves right, offset the label to the left. + if (this.points[1].x > srcP.x) + offsetX = -(LABEL_PADDING + labelW); + } + } else { + // Failsafe offset calculation if no source node is present. + if (this.points[0].x > this.points[1].x) + offsetX = -(labelW + LABEL_PADDING); + if (this.points[0].y <= this.points[1].y) + offsetY = labelH + LABEL_PADDING; + } + + ctx.fillStyle = this.getCssProperty( + renderer, '--interstate-edge-color' ); + for (let i = 0; i < labelLines.length; i++) + ctx.fillText( + labelLines[i], + srcP.x + offsetX, + (srcP.y + offsetY) - (i * (labelHs[0] + SDFV.LINEHEIGHT)) + ); ctx.font = oldFont; } @@ -2661,6 +2756,6 @@ export const SDFGElements: { [name: string]: typeof SDFGElement } = { ControlFlowBlock, BasicBlock, State, - ScopeBlock, - LoopScopeBlock, + ControlFlowRegion: ControlFlowRegion, + LoopRegion: LoopRegion, }; diff --git a/src/utils/sdfg/sdfg_parser.ts b/src/utils/sdfg/sdfg_parser.ts index b23653da..b0766358 100644 --- a/src/utils/sdfg/sdfg_parser.ts +++ b/src/utils/sdfg/sdfg_parser.ts @@ -128,7 +128,7 @@ export class SDFGStateParser { switch (x.type) { case SDFGElementType.SDFGState: case SDFGElementType.BasicBlock: - case SDFGElementType.LoopScopeBlock: + case SDFGElementType.LoopRegion: return new SDFGStateParser(x as JsonSDFGBlock); default: return new SDFGNodeParser(x as JsonSDFGNode);