From 45295a3843febdc5458ec894dbcc80fe591faf29 Mon Sep 17 00:00:00 2001 From: David Evans Date: Mon, 23 Oct 2017 20:31:24 +0100 Subject: [PATCH] Simplify connection handling --- README.md | 6 +- scripts/sequence/Parser.js | 34 ++++----- scripts/sequence/Parser_spec.js | 105 ++++++++++++++++++++++----- scripts/sequence/Renderer.js | 122 ++++++++++++++------------------ 4 files changed, 161 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 8cd5168..4608de6 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ Bowie -> Audience: Sings title Connection Types Foo -> Bar: Simple arrow -Foo --> Bar: Dotted arrow +Foo --> Bar: Dashed arrow Foo <- Bar: Reversed arrow -Foo <-- Bar: Reversed dotted arrow +Foo <-- Bar: Reversed dashed arrow Foo <-> Bar: Double arrow -Foo <--> Bar: Double dotted arrow +Foo <--> Bar: Double dashed arrow # An arrow with no label: Foo -> Bar diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index 6990590..ccca8a3 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -32,14 +32,14 @@ define(() => { 'repeat': {type: 'block begin', mode: 'repeat', skip: []}, }; - const CONNECTION_TYPES = [ - '->', - '<-', - '<->', - '-->', - '<--', - '<-->', - ]; + const CONNECTION_TYPES = { + '->': {line: 'solid', left: false, right: true}, + '<-': {line: 'solid', left: true, right: false}, + '<->': {line: 'solid', left: true, right: true}, + '-->': {line: 'dash', left: false, right: true}, + '<--': {line: 'dash', left: true, right: false}, + '<-->': {line: 'dash', left: true, right: true}, + }; const TERMINATOR_TYPES = [ 'none', @@ -226,24 +226,26 @@ define(() => { labelSplit = line.length; } let typeSplit = -1; - for(let j = 0; j < CONNECTION_TYPES.length; ++ j) { - const p = line.indexOf(CONNECTION_TYPES[j]); - if(p !== -1 && p < labelSplit) { - typeSplit = p; + let options = null; + for(let j = 0; j < line.length; ++ j) { + const opts = CONNECTION_TYPES[line[j]]; + if(opts) { + typeSplit = j; + options = opts; break; } } - if(typeSplit <= 0 || typeSplit === labelSplit - 1) { + if(typeSplit <= 0 || typeSplit >= labelSplit - 1) { return null; } - return { - type: line[typeSplit], + return Object.assign({ + type: 'connection', agents: [ line.slice(0, typeSplit).join(' '), line.slice(typeSplit + 1, labelSplit).join(' '), ], label: line.slice(labelSplit + 1).join(' '), - }; + }, options); } function parseMeta(line, meta) { diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 420b379..490d727 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -103,6 +103,17 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { }); }); + function connectionStage(agents, label = '') { + return { + type: 'connection', + line: 'solid', + left: false, + right: true, + agents, + label, + }; + } + describe('.parse', () => { it('returns an empty sequence for blank input', () => { const parsed = parser.parse(''); @@ -133,37 +144,37 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { it('converts entries into abstract form', () => { const parsed = parser.parse('A -> B'); expect(parsed.stages).toEqual([ - {type: '->', agents: ['A', 'B'], label: ''}, + connectionStage(['A', 'B']), ]); }); it('combines multiple tokens into single entries', () => { const parsed = parser.parse('A B -> C D'); expect(parsed.stages).toEqual([ - {type: '->', agents: ['A B', 'C D'], label: ''}, + connectionStage(['A B', 'C D']), ]); }); it('parses optional labels', () => { const parsed = parser.parse('A B -> C D: foo bar'); expect(parsed.stages).toEqual([ - {type: '->', agents: ['A B', 'C D'], label: 'foo bar'}, + connectionStage(['A B', 'C D'], 'foo bar'), ]); }); it('converts multiple entries', () => { const parsed = parser.parse('A -> B\nB -> A'); expect(parsed.stages).toEqual([ - {type: '->', agents: ['A', 'B'], label: ''}, - {type: '->', agents: ['B', 'A'], label: ''}, + connectionStage(['A', 'B']), + connectionStage(['B', 'A']), ]); }); it('ignores blank lines', () => { const parsed = parser.parse('A -> B\n\nB -> A\n'); expect(parsed.stages).toEqual([ - {type: '->', agents: ['A', 'B'], label: ''}, - {type: '->', agents: ['B', 'A'], label: ''}, + connectionStage(['A', 'B']), + connectionStage(['B', 'A']), ]); }); @@ -177,12 +188,54 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { 'A <--> B\n' ); expect(parsed.stages).toEqual([ - {type: '->', agents: ['A', 'B'], label: ''}, - {type: '<-', agents: ['A', 'B'], label: ''}, - {type: '<->', agents: ['A', 'B'], label: ''}, - {type: '-->', agents: ['A', 'B'], label: ''}, - {type: '<--', agents: ['A', 'B'], label: ''}, - {type: '<-->', agents: ['A', 'B'], label: ''}, + { + type: 'connection', + line: 'solid', + left: false, + right: true, + agents: ['A', 'B'], + label: '', + }, + { + type: 'connection', + line: 'solid', + left: true, + right: false, + agents: ['A', 'B'], + label: '', + }, + { + type: 'connection', + line: 'solid', + left: true, + right: true, + agents: ['A', 'B'], + label: '', + }, + { + type: 'connection', + line: 'dash', + left: false, + right: true, + agents: ['A', 'B'], + label: '', + }, + { + type: 'connection', + line: 'dash', + left: true, + right: false, + agents: ['A', 'B'], + label: '', + }, + { + type: 'connection', + line: 'dash', + left: true, + right: true, + agents: ['A', 'B'], + label: '', + }, ]); }); @@ -192,8 +245,22 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { 'A -> B: B <- A\n' ); expect(parsed.stages).toEqual([ - {type: '<-', agents: ['A', 'B'], label: 'B -> A'}, - {type: '->', agents: ['A', 'B'], label: 'B <- A'}, + { + type: 'connection', + line: 'solid', + left: true, + right: false, + agents: ['A', 'B'], + label: 'B -> A', + }, + { + type: 'connection', + line: 'solid', + left: false, + right: true, + agents: ['A', 'B'], + label: 'B <- A', + }, ]); }); @@ -299,12 +366,12 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { ); expect(parsed.stages).toEqual([ {type: 'block begin', mode: 'if', label: 'something happens'}, - {type: '->', agents: ['A', 'B'], label: ''}, + connectionStage(['A', 'B']), {type: 'block split', mode: 'else', label: 'something else'}, - {type: '->', agents: ['A', 'C'], label: ''}, - {type: '->', agents: ['C', 'B'], label: ''}, + connectionStage(['A', 'C']), + connectionStage(['C', 'B']), {type: 'block split', mode: 'else', label: ''}, - {type: '->', agents: ['A', 'D'], label: ''}, + connectionStage(['A', 'D']), {type: 'block end'}, ]); }); diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 1020faf..72acb2a 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -42,17 +42,18 @@ define(() => { const AGENT_CROSS_SIZE = 20; const AGENT_NONE_HEIGHT = 10; const ACTION_MARGIN = 5; - const ARROW_HEIGHT = 8; - const ARROW_POINT = 4; - const ARROW_LABEL_PADDING = 6; - const ARROW_LABEL_MASK_PADDING = 3; - const ARROW_LABEL_MARGIN_TOP = 2; - const ARROW_LABEL_MARGIN_BOTTOM = 1; + const CONNECT_HEIGHT = 8; + const CONNECT_POINT = 4; + const CONNECT_LABEL_PADDING = 6; + const CONNECT_LABEL_MASK_PADDING = 3; + const CONNECT_LABEL_MARGIN_TOP = 2; + const CONNECT_LABEL_MARGIN_BOTTOM = 1; const ATTRS = { TITLE: { 'font-family': 'sans-serif', 'font-size': 20, + 'text-anchor': 'middle', 'class': 'title', }, @@ -70,6 +71,7 @@ define(() => { AGENT_BOX_LABEL: { 'font-family': 'sans-serif', 'font-size': 12, + 'text-anchor': 'middle', }, AGENT_CROSS: { 'fill': 'none', @@ -81,25 +83,26 @@ define(() => { 'height': 5, }, - ARROW_LINE_SOLID: { + CONNECT_LINE_SOLID: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }, - ARROW_LINE_DASH: { + CONNECT_LINE_DASH: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, 'stroke-dasharray': '2, 2', }, - ARROW_LABEL: { + CONNECT_LABEL: { 'font-family': 'sans-serif', 'font-size': 8, + 'text-anchor': 'middle', }, - ARROW_LABEL_MASK: { + CONNECT_LABEL_MASK: { 'fill': '#FFFFFF', }, - ARROW_HEAD: { + CONNECT_HEAD: { 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 1, @@ -167,24 +170,7 @@ define(() => { this.renderAction = { 'agent begin': this.renderAgentBegin.bind(this), 'agent end': this.renderAgentEnd.bind(this), - '->': this.renderArrow.bind(this, { - lineAttrs: ATTRS.ARROW_LINE_SOLID, left: false, right: true, - }), - '<-': this.renderArrow.bind(this, { - lineAttrs: ATTRS.ARROW_LINE_SOLID, left: true, right: false, - }), - '<->': this.renderArrow.bind(this, { - lineAttrs: ATTRS.ARROW_LINE_SOLID, left: true, right: true, - }), - '-->': this.renderArrow.bind(this, { - lineAttrs: ATTRS.ARROW_LINE_DASH, left: false, right: true, - }), - '<--': this.renderArrow.bind(this, { - lineAttrs: ATTRS.ARROW_LINE_DASH, left: true, right: false, - }), - '<-->': this.renderArrow.bind(this, { - lineAttrs: ATTRS.ARROW_LINE_DASH, left: true, right: true, - }), + 'connection': this.renderConnection.bind(this), 'block': this.renderBlock.bind(this), 'note over': this.renderNoteOver.bind(this), 'note left': this.renderNoteLeft.bind(this), @@ -195,12 +181,7 @@ define(() => { this.separationAction = { 'agent begin': this.separationAgentCap.bind(this), 'agent end': this.separationAgentCap.bind(this), - '->': this.separationArrow.bind(this), - '<-': this.separationArrow.bind(this), - '<->': this.separationArrow.bind(this), - '-->': this.separationArrow.bind(this), - '<--': this.separationArrow.bind(this), - '<-->': this.separationArrow.bind(this), + 'connection': this.separationConnection.bind(this), 'block': this.separationBlock.bind(this), 'note over': this.separationNoteOver.bind(this), 'note left': this.separationNoteLeft.bind(this), @@ -277,11 +258,11 @@ define(() => { } } - separationArrow(agentInfos, stage) { + separationConnection(agentInfos, stage) { const w = ( - this.testTextWidth(this.testArrowWidth, stage.label) + - ARROW_POINT * 2 + - ARROW_LABEL_PADDING * 2 + + this.testTextWidth(this.testConnectWidth, stage.label) + + CONNECT_POINT * 2 + + CONNECT_LABEL_PADDING * 2 + ATTRS.AGENT_LINE['stroke-width'] ); const agent1 = stage.agents[0]; @@ -322,7 +303,7 @@ define(() => { }, ATTRS.AGENT_BOX))); const name = makeSVGNode('text', Object.assign({ - 'x': x - labelWidth / 2 + BOX_PADDING, + 'x': x, 'y': this.currentY + ( ATTRS.AGENT_BOX.height + ATTRS.AGENT_BOX_LABEL['font-size'] * (2 - LINE_HEIGHT) @@ -407,42 +388,47 @@ define(() => { this.currentY += shifts.height + ACTION_MARGIN; } - renderArrow({lineAttrs, left, right}, agentInfos, stage) { + renderConnection(agentInfos, {label, agents, line, left, right}) { /* jshint -W074, -W071 */ // TODO: tidy this up - const from = agentInfos.get(stage.agents[0]); - const to = agentInfos.get(stage.agents[1]); + const from = agentInfos.get(agents[0]); + const to = agentInfos.get(agents[1]); - const dy = ARROW_HEIGHT / 2; - const dx = ARROW_POINT; + const dy = CONNECT_HEIGHT / 2; + const dx = CONNECT_POINT; const dir = (from.x < to.x) ? 1 : -1; const short = ATTRS.AGENT_LINE['stroke-width']; let y = this.currentY; - if(stage.label) { - const mask = makeSVGNode('rect', ATTRS.ARROW_LABEL_MASK); - const label = makeSVGNode('text', ATTRS.ARROW_LABEL); - label.appendChild(makeText(stage.label)); - const sz = ATTRS.ARROW_LABEL['font-size']; + const lineAttrs = { + 'solid': ATTRS.CONNECT_LINE_SOLID, + 'dash': ATTRS.CONNECT_LINE_DASH, + }; + + if(label) { + const mask = makeSVGNode('rect', ATTRS.CONNECT_LABEL_MASK); + const labelNode = makeSVGNode('text', ATTRS.CONNECT_LABEL); + labelNode.appendChild(makeText(label)); + const sz = ATTRS.CONNECT_LABEL['font-size']; this.actions.appendChild(mask); - this.actions.appendChild(label); + this.actions.appendChild(labelNode); y += Math.max( dy, - ARROW_LABEL_MARGIN_TOP + + CONNECT_LABEL_MARGIN_TOP + sz * LINE_HEIGHT + - ARROW_LABEL_MARGIN_BOTTOM + CONNECT_LABEL_MARGIN_BOTTOM ); - const w = label.getComputedTextLength(); - const x = (from.x + to.x - w) / 2; + const w = labelNode.getComputedTextLength(); + const x = (from.x + to.x) / 2; const yBase = ( y - sz * (LINE_HEIGHT - 1) - - ARROW_LABEL_MARGIN_BOTTOM + CONNECT_LABEL_MARGIN_BOTTOM ); - label.setAttribute('x', x); - label.setAttribute('y', yBase); - mask.setAttribute('x', x - ARROW_LABEL_MASK_PADDING); + labelNode.setAttribute('x', x); + labelNode.setAttribute('y', yBase); + mask.setAttribute('x', x - w / 2 - CONNECT_LABEL_MASK_PADDING); mask.setAttribute('y', yBase - sz); - mask.setAttribute('width', w + ARROW_LABEL_MASK_PADDING * 2); + mask.setAttribute('width', w + CONNECT_LABEL_MASK_PADDING * 2); mask.setAttribute('height', sz * LINE_HEIGHT); } else { y += dy; @@ -453,7 +439,7 @@ define(() => { 'M ' + (from.x + (left ? short : 0) * dir) + ' ' + y + ' L ' + (to.x - (right ? short : 0) * dir) + ' ' + y ), - }, lineAttrs))); + }, lineAttrs[line]))); if(left) { this.actions.appendChild(makeSVGNode('path', Object.assign({ @@ -461,9 +447,9 @@ define(() => { 'M ' + (from.x + (dx + short) * dir) + ' ' + (y - dy) + ' L ' + (from.x + short * dir) + ' ' + y + ' L ' + (from.x + (dx + short) * dir) + ' ' + (y + dy) + - (ATTRS.ARROW_HEAD.fill === 'none' ? '' : ' Z') + (ATTRS.CONNECT_HEAD.fill === 'none' ? '' : ' Z') ), - }, ATTRS.ARROW_HEAD))); + }, ATTRS.CONNECT_HEAD))); } if(right) { @@ -472,9 +458,9 @@ define(() => { 'M ' + (to.x - (dx + short) * dir) + ' ' + (y - dy) + ' L ' + (to.x - short * dir) + ' ' + y + ' L ' + (to.x - (dx + short) * dir) + ' ' + (y + dy) + - (ATTRS.ARROW_HEAD.fill === 'none' ? '' : ' Z') + (ATTRS.CONNECT_HEAD.fill === 'none' ? '' : ' Z') ), - }, ATTRS.ARROW_HEAD))); + }, ATTRS.CONNECT_HEAD))); } this.currentY = y + dy + ACTION_MARGIN; @@ -542,10 +528,10 @@ define(() => { this.removeTextTester(testNameWidth); - this.testArrowWidth = this.makeTextTester(ATTRS.ARROW_LABEL); + this.testConnectWidth = this.makeTextTester(ATTRS.CONNECT_LABEL); this.visibleAgents = ['[', ']']; traverse(stages, this.checkSeparation.bind(this, agentInfos)); - this.removeTextTester(this.testArrowWidth); + this.removeTextTester(this.testConnectWidth); let currentX = 0; agents.forEach((agent) => { @@ -585,7 +571,7 @@ define(() => { ) + ')' ); - this.title.setAttribute('x', (width - titleWidth) / 2); + this.title.setAttribute('x', width / 2); this.base.setAttribute('viewBox', '0 0 ' + width + ' ' + height); this.width = width; this.height = height;