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
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

View File

@ -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) {

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', () => {
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'},
]);
});

View File

@ -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;