Simplify connection handling

This commit is contained in:
David Evans 2017-10-23 20:31:24 +01:00
parent 711019275e
commit 45295a3843
4 changed files with 161 additions and 106 deletions

View File

@ -35,11 +35,11 @@ Bowie -> Audience: Sings
title Connection Types title Connection Types
Foo -> Bar: Simple arrow Foo -> Bar: Simple arrow
Foo --> Bar: Dotted arrow Foo --> Bar: Dashed arrow
Foo <- Bar: Reversed arrow Foo <- Bar: Reversed arrow
Foo <-- Bar: Reversed dotted arrow Foo <-- Bar: Reversed dashed arrow
Foo <-> Bar: Double arrow Foo <-> Bar: Double arrow
Foo <--> Bar: Double dotted arrow Foo <--> Bar: Double dashed arrow
# An arrow with no label: # An arrow with no label:
Foo -> Bar Foo -> Bar

View File

@ -32,14 +32,14 @@ define(() => {
'repeat': {type: 'block begin', mode: 'repeat', skip: []}, '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 = [ const TERMINATOR_TYPES = [
'none', 'none',
@ -226,24 +226,26 @@ define(() => {
labelSplit = line.length; labelSplit = line.length;
} }
let typeSplit = -1; let typeSplit = -1;
for(let j = 0; j < CONNECTION_TYPES.length; ++ j) { let options = null;
const p = line.indexOf(CONNECTION_TYPES[j]); for(let j = 0; j < line.length; ++ j) {
if(p !== -1 && p < labelSplit) { const opts = CONNECTION_TYPES[line[j]];
typeSplit = p; if(opts) {
typeSplit = j;
options = opts;
break; break;
} }
} }
if(typeSplit <= 0 || typeSplit === labelSplit - 1) { if(typeSplit <= 0 || typeSplit >= labelSplit - 1) {
return null; return null;
} }
return { return Object.assign({
type: line[typeSplit], type: 'connection',
agents: [ agents: [
line.slice(0, typeSplit).join(' '), line.slice(0, typeSplit).join(' '),
line.slice(typeSplit + 1, labelSplit).join(' '), line.slice(typeSplit + 1, labelSplit).join(' '),
], ],
label: line.slice(labelSplit + 1).join(' '), label: line.slice(labelSplit + 1).join(' '),
}; }, options);
} }
function parseMeta(line, meta) { function parseMeta(line, meta) {

View File

@ -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', () => { describe('.parse', () => {
it('returns an empty sequence for blank input', () => { it('returns an empty sequence for blank input', () => {
const parsed = parser.parse(''); const parsed = parser.parse('');
@ -133,37 +144,37 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
it('converts entries into abstract form', () => { it('converts entries into abstract form', () => {
const parsed = parser.parse('A -> B'); const parsed = parser.parse('A -> B');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: '->', agents: ['A', 'B'], label: ''}, connectionStage(['A', 'B']),
]); ]);
}); });
it('combines multiple tokens into single entries', () => { it('combines multiple tokens into single entries', () => {
const parsed = parser.parse('A B -> C D'); const parsed = parser.parse('A B -> C D');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: '->', agents: ['A B', 'C D'], label: ''}, connectionStage(['A B', 'C D']),
]); ]);
}); });
it('parses optional labels', () => { it('parses optional labels', () => {
const parsed = parser.parse('A B -> C D: foo bar'); const parsed = parser.parse('A B -> C D: foo bar');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: '->', agents: ['A B', 'C D'], label: 'foo bar'}, connectionStage(['A B', 'C D'], 'foo bar'),
]); ]);
}); });
it('converts multiple entries', () => { it('converts multiple entries', () => {
const parsed = parser.parse('A -> B\nB -> A'); const parsed = parser.parse('A -> B\nB -> A');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: '->', agents: ['A', 'B'], label: ''}, connectionStage(['A', 'B']),
{type: '->', agents: ['B', 'A'], label: ''}, connectionStage(['B', 'A']),
]); ]);
}); });
it('ignores blank lines', () => { it('ignores blank lines', () => {
const parsed = parser.parse('A -> B\n\nB -> A\n'); const parsed = parser.parse('A -> B\n\nB -> A\n');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: '->', agents: ['A', 'B'], label: ''}, connectionStage(['A', 'B']),
{type: '->', agents: ['B', 'A'], label: ''}, connectionStage(['B', 'A']),
]); ]);
}); });
@ -177,12 +188,54 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
'A <--> B\n' 'A <--> B\n'
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: '->', agents: ['A', 'B'], label: ''}, {
{type: '<-', agents: ['A', 'B'], label: ''}, type: 'connection',
{type: '<->', agents: ['A', 'B'], label: ''}, line: 'solid',
{type: '-->', agents: ['A', 'B'], label: ''}, left: false,
{type: '<--', agents: ['A', 'B'], label: ''}, right: true,
{type: '<-->', agents: ['A', 'B'], label: ''}, 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' 'A -> B: B <- A\n'
); );
expect(parsed.stages).toEqual([ 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([ expect(parsed.stages).toEqual([
{type: 'block begin', mode: 'if', label: 'something happens'}, {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: 'block split', mode: 'else', label: 'something else'},
{type: '->', agents: ['A', 'C'], label: ''}, connectionStage(['A', 'C']),
{type: '->', agents: ['C', 'B'], label: ''}, connectionStage(['C', 'B']),
{type: 'block split', mode: 'else', label: ''}, {type: 'block split', mode: 'else', label: ''},
{type: '->', agents: ['A', 'D'], label: ''}, connectionStage(['A', 'D']),
{type: 'block end'}, {type: 'block end'},
]); ]);
}); });

View File

@ -42,17 +42,18 @@ define(() => {
const AGENT_CROSS_SIZE = 20; const AGENT_CROSS_SIZE = 20;
const AGENT_NONE_HEIGHT = 10; const AGENT_NONE_HEIGHT = 10;
const ACTION_MARGIN = 5; const ACTION_MARGIN = 5;
const ARROW_HEIGHT = 8; const CONNECT_HEIGHT = 8;
const ARROW_POINT = 4; const CONNECT_POINT = 4;
const ARROW_LABEL_PADDING = 6; const CONNECT_LABEL_PADDING = 6;
const ARROW_LABEL_MASK_PADDING = 3; const CONNECT_LABEL_MASK_PADDING = 3;
const ARROW_LABEL_MARGIN_TOP = 2; const CONNECT_LABEL_MARGIN_TOP = 2;
const ARROW_LABEL_MARGIN_BOTTOM = 1; const CONNECT_LABEL_MARGIN_BOTTOM = 1;
const ATTRS = { const ATTRS = {
TITLE: { TITLE: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 20, 'font-size': 20,
'text-anchor': 'middle',
'class': 'title', 'class': 'title',
}, },
@ -70,6 +71,7 @@ define(() => {
AGENT_BOX_LABEL: { AGENT_BOX_LABEL: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 12, 'font-size': 12,
'text-anchor': 'middle',
}, },
AGENT_CROSS: { AGENT_CROSS: {
'fill': 'none', 'fill': 'none',
@ -81,25 +83,26 @@ define(() => {
'height': 5, 'height': 5,
}, },
ARROW_LINE_SOLID: { CONNECT_LINE_SOLID: {
'fill': 'none', 'fill': 'none',
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 1, 'stroke-width': 1,
}, },
ARROW_LINE_DASH: { CONNECT_LINE_DASH: {
'fill': 'none', 'fill': 'none',
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 1, 'stroke-width': 1,
'stroke-dasharray': '2, 2', 'stroke-dasharray': '2, 2',
}, },
ARROW_LABEL: { CONNECT_LABEL: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 8, 'font-size': 8,
'text-anchor': 'middle',
}, },
ARROW_LABEL_MASK: { CONNECT_LABEL_MASK: {
'fill': '#FFFFFF', 'fill': '#FFFFFF',
}, },
ARROW_HEAD: { CONNECT_HEAD: {
'fill': '#000000', 'fill': '#000000',
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 1, 'stroke-width': 1,
@ -167,24 +170,7 @@ define(() => {
this.renderAction = { this.renderAction = {
'agent begin': this.renderAgentBegin.bind(this), 'agent begin': this.renderAgentBegin.bind(this),
'agent end': this.renderAgentEnd.bind(this), 'agent end': this.renderAgentEnd.bind(this),
'->': this.renderArrow.bind(this, { 'connection': this.renderConnection.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,
}),
'block': this.renderBlock.bind(this), 'block': this.renderBlock.bind(this),
'note over': this.renderNoteOver.bind(this), 'note over': this.renderNoteOver.bind(this),
'note left': this.renderNoteLeft.bind(this), 'note left': this.renderNoteLeft.bind(this),
@ -195,12 +181,7 @@ define(() => {
this.separationAction = { this.separationAction = {
'agent begin': this.separationAgentCap.bind(this), 'agent begin': this.separationAgentCap.bind(this),
'agent end': this.separationAgentCap.bind(this), 'agent end': this.separationAgentCap.bind(this),
'->': this.separationArrow.bind(this), 'connection': this.separationConnection.bind(this),
'<-': this.separationArrow.bind(this),
'<->': this.separationArrow.bind(this),
'-->': this.separationArrow.bind(this),
'<--': this.separationArrow.bind(this),
'<-->': this.separationArrow.bind(this),
'block': this.separationBlock.bind(this), 'block': this.separationBlock.bind(this),
'note over': this.separationNoteOver.bind(this), 'note over': this.separationNoteOver.bind(this),
'note left': this.separationNoteLeft.bind(this), 'note left': this.separationNoteLeft.bind(this),
@ -277,11 +258,11 @@ define(() => {
} }
} }
separationArrow(agentInfos, stage) { separationConnection(agentInfos, stage) {
const w = ( const w = (
this.testTextWidth(this.testArrowWidth, stage.label) + this.testTextWidth(this.testConnectWidth, stage.label) +
ARROW_POINT * 2 + CONNECT_POINT * 2 +
ARROW_LABEL_PADDING * 2 + CONNECT_LABEL_PADDING * 2 +
ATTRS.AGENT_LINE['stroke-width'] ATTRS.AGENT_LINE['stroke-width']
); );
const agent1 = stage.agents[0]; const agent1 = stage.agents[0];
@ -322,7 +303,7 @@ define(() => {
}, ATTRS.AGENT_BOX))); }, ATTRS.AGENT_BOX)));
const name = makeSVGNode('text', Object.assign({ const name = makeSVGNode('text', Object.assign({
'x': x - labelWidth / 2 + BOX_PADDING, 'x': x,
'y': this.currentY + ( 'y': this.currentY + (
ATTRS.AGENT_BOX.height + ATTRS.AGENT_BOX.height +
ATTRS.AGENT_BOX_LABEL['font-size'] * (2 - LINE_HEIGHT) ATTRS.AGENT_BOX_LABEL['font-size'] * (2 - LINE_HEIGHT)
@ -407,42 +388,47 @@ define(() => {
this.currentY += shifts.height + ACTION_MARGIN; 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 /* jshint -W074, -W071 */ // TODO: tidy this up
const from = agentInfos.get(stage.agents[0]); const from = agentInfos.get(agents[0]);
const to = agentInfos.get(stage.agents[1]); const to = agentInfos.get(agents[1]);
const dy = ARROW_HEIGHT / 2; const dy = CONNECT_HEIGHT / 2;
const dx = ARROW_POINT; const dx = CONNECT_POINT;
const dir = (from.x < to.x) ? 1 : -1; const dir = (from.x < to.x) ? 1 : -1;
const short = ATTRS.AGENT_LINE['stroke-width']; const short = ATTRS.AGENT_LINE['stroke-width'];
let y = this.currentY; let y = this.currentY;
if(stage.label) { const lineAttrs = {
const mask = makeSVGNode('rect', ATTRS.ARROW_LABEL_MASK); 'solid': ATTRS.CONNECT_LINE_SOLID,
const label = makeSVGNode('text', ATTRS.ARROW_LABEL); 'dash': ATTRS.CONNECT_LINE_DASH,
label.appendChild(makeText(stage.label)); };
const sz = ATTRS.ARROW_LABEL['font-size'];
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(mask);
this.actions.appendChild(label); this.actions.appendChild(labelNode);
y += Math.max( y += Math.max(
dy, dy,
ARROW_LABEL_MARGIN_TOP + CONNECT_LABEL_MARGIN_TOP +
sz * LINE_HEIGHT + sz * LINE_HEIGHT +
ARROW_LABEL_MARGIN_BOTTOM CONNECT_LABEL_MARGIN_BOTTOM
); );
const w = label.getComputedTextLength(); const w = labelNode.getComputedTextLength();
const x = (from.x + to.x - w) / 2; const x = (from.x + to.x) / 2;
const yBase = ( const yBase = (
y - y -
sz * (LINE_HEIGHT - 1) - sz * (LINE_HEIGHT - 1) -
ARROW_LABEL_MARGIN_BOTTOM CONNECT_LABEL_MARGIN_BOTTOM
); );
label.setAttribute('x', x); labelNode.setAttribute('x', x);
label.setAttribute('y', yBase); labelNode.setAttribute('y', yBase);
mask.setAttribute('x', x - ARROW_LABEL_MASK_PADDING); mask.setAttribute('x', x - w / 2 - CONNECT_LABEL_MASK_PADDING);
mask.setAttribute('y', yBase - sz); 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); mask.setAttribute('height', sz * LINE_HEIGHT);
} else { } else {
y += dy; y += dy;
@ -453,7 +439,7 @@ define(() => {
'M ' + (from.x + (left ? short : 0) * dir) + ' ' + y + 'M ' + (from.x + (left ? short : 0) * dir) + ' ' + y +
' L ' + (to.x - (right ? short : 0) * dir) + ' ' + y ' L ' + (to.x - (right ? short : 0) * dir) + ' ' + y
), ),
}, lineAttrs))); }, lineAttrs[line])));
if(left) { if(left) {
this.actions.appendChild(makeSVGNode('path', Object.assign({ this.actions.appendChild(makeSVGNode('path', Object.assign({
@ -461,9 +447,9 @@ define(() => {
'M ' + (from.x + (dx + short) * dir) + ' ' + (y - dy) + 'M ' + (from.x + (dx + short) * dir) + ' ' + (y - dy) +
' L ' + (from.x + short * dir) + ' ' + y + ' L ' + (from.x + short * dir) + ' ' + y +
' L ' + (from.x + (dx + short) * dir) + ' ' + (y + dy) + ' 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) { if(right) {
@ -472,9 +458,9 @@ define(() => {
'M ' + (to.x - (dx + short) * dir) + ' ' + (y - dy) + 'M ' + (to.x - (dx + short) * dir) + ' ' + (y - dy) +
' L ' + (to.x - short * dir) + ' ' + y + ' L ' + (to.x - short * dir) + ' ' + y +
' L ' + (to.x - (dx + short) * dir) + ' ' + (y + dy) + ' 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; this.currentY = y + dy + ACTION_MARGIN;
@ -542,10 +528,10 @@ define(() => {
this.removeTextTester(testNameWidth); this.removeTextTester(testNameWidth);
this.testArrowWidth = this.makeTextTester(ATTRS.ARROW_LABEL); this.testConnectWidth = this.makeTextTester(ATTRS.CONNECT_LABEL);
this.visibleAgents = ['[', ']']; this.visibleAgents = ['[', ']'];
traverse(stages, this.checkSeparation.bind(this, agentInfos)); traverse(stages, this.checkSeparation.bind(this, agentInfos));
this.removeTextTester(this.testArrowWidth); this.removeTextTester(this.testConnectWidth);
let currentX = 0; let currentX = 0;
agents.forEach((agent) => { 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.base.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
this.width = width; this.width = width;
this.height = height; this.height = height;