Simplify connection handling
This commit is contained in:
parent
711019275e
commit
45295a3843
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue