Add support for wavy connection lines [#24]

This commit is contained in:
David Evans 2017-11-12 14:14:56 +00:00
parent 16095cf78a
commit 5b3d0af311
12 changed files with 235 additions and 82 deletions

View File

@ -65,7 +65,7 @@ Foo -> +Bar: Foo asks Bar
[ <- Foo: To the left [ <- Foo: To the left
Foo -> ]: To the right Foo -> ]: To the right
Foo <- ]: From the right Foo <- ]: From the right
[ -> ]: Left to right! [ ~> ]: Wavy left to right!
# (etc.) # (etc.)
``` ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -56,24 +56,28 @@ define(() => {
return list[list.length - 1]; return list[list.length - 1];
} }
function combineRecur(parts, position, str, target) { function combineRecur(parts, position, current, target) {
if(position >= parts.length) { if(position >= parts.length) {
target.push(str); target.push(current.slice());
return; return;
} }
const choices = parts[position]; const choices = parts[position];
if(!Array.isArray(choices)) { if(!Array.isArray(choices)) {
combineRecur(parts, position + 1, str + choices, target); current.push(choices);
combineRecur(parts, position + 1, current, target);
current.pop();
return; return;
} }
for(let i = 0; i < choices.length; ++ i) { for(let i = 0; i < choices.length; ++ i) {
combineRecur(parts, position + 1, str + choices[i], target); current.push(choices[i]);
combineRecur(parts, position + 1, current, target);
current.pop();
} }
} }
function combine(parts) { function combine(parts) {
const target = []; const target = [];
combineRecur(parts, 0, '', target); combineRecur(parts, 0, [], target);
return target; return target;
} }

View File

@ -179,10 +179,10 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => {
['Ff'], ['Ff'],
]); ]);
expect(list).toEqual([ expect(list).toEqual([
'AaCcEeFf', ['Aa', 'Cc', 'Ee', 'Ff'],
'AaDdEeFf', ['Aa', 'Dd', 'Ee', 'Ff'],
'BbCcEeFf', ['Bb', 'Cc', 'Ee', 'Ff'],
'BbDdEeFf', ['Bb', 'Dd', 'Ee', 'Ff'],
]); ]);
}); });
}); });

View File

@ -53,6 +53,10 @@
title: 'Open arrow', title: 'Open arrow',
code: '{Agent1} ->> {Agent2}: {Message}', code: '{Agent1} ->> {Agent2}: {Message}',
}, },
{
title: 'Wavy line',
code: '{Agent1} ~> {Agent2}: {Message}',
},
{ {
title: 'Self-connection', title: 'Self-connection',
code: '{Agent1} -> {Agent1}: {Message}', code: '{Agent1} -> {Agent1}: {Message}',

View File

@ -3,17 +3,10 @@ define(['core/ArrayUtilities'], (array) => {
const CM_ERROR = {type: 'error line-error', then: {'': 0}}; const CM_ERROR = {type: 'error line-error', then: {'': 0}};
const CM_COMMANDS = ((() => { const makeCommands = ((() => {
const end = {type: '', suggest: '\n', then: {}}; const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', then: {}}; const hiddenEnd = {type: '', then: {}};
const ARROWS = array.combine([
['', '<', '<<'],
['-', '--'],
['', '>', '>>'],
]);
array.removeAll(ARROWS, ['-', '--']);
const textToEnd = {type: 'string', then: {'': 0, '\n': end}}; const textToEnd = {type: 'string', then: {'': 0, '\n': end}};
const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: { const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
@ -108,7 +101,7 @@ define(['core/ArrayUtilities'], (array) => {
}; };
} }
function makeCMConnect() { function makeCMConnect(arrows) {
const connect = { const connect = {
type: 'keyword', type: 'keyword',
suggest: true, suggest: true,
@ -124,11 +117,11 @@ define(['core/ArrayUtilities'], (array) => {
}, },
'': 0, '': 0,
}; };
ARROWS.forEach((arrow) => (then[arrow] = connect)); arrows.forEach((arrow) => (then[arrow] = connect));
return makeOpBlock({type: 'variable', suggest: 'Agent', then}); return makeOpBlock({type: 'variable', suggest: 'Agent', then});
} }
return {type: 'error line-error', then: Object.assign({ const BASE_THEN = {
'title': {type: 'keyword', suggest: true, then: { 'title': {type: 'keyword', suggest: true, then: {
'': textToEnd, '': textToEnd,
}}, }},
@ -220,7 +213,14 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
}}, }},
}}, }},
}, makeCMConnect())}; };
return (arrows) => {
return {
type: 'error line-error',
then: Object.assign(BASE_THEN, makeCMConnect(arrows)),
};
};
})()); })());
function cmCappedToken(token, current) { function cmCappedToken(token, current) {
@ -294,12 +294,12 @@ define(['core/ArrayUtilities'], (array) => {
} }
} }
function cmCheckToken(state, eol) { function cmCheckToken(state, eol, commands) {
const suggestions = { const suggestions = {
type: '', type: '',
value: '', value: '',
}; };
let current = CM_COMMANDS; let current = commands;
const path = [current]; const path = [current];
state.line.forEach((token, i) => { state.line.forEach((token, i) => {
@ -336,8 +336,9 @@ define(['core/ArrayUtilities'], (array) => {
} }
return class Mode { return class Mode {
constructor(tokenDefinitions) { constructor(tokenDefinitions, arrows) {
this.tokenDefinitions = tokenDefinitions; this.tokenDefinitions = tokenDefinitions;
this.commands = makeCommands(arrows);
this.lineComment = '#'; this.lineComment = '#';
} }
@ -348,7 +349,7 @@ define(['core/ArrayUtilities'], (array) => {
currentQuoted: false, currentQuoted: false,
knownAgent: [], knownAgent: [],
knownLabel: [], knownLabel: [],
beginCompletions: cmMakeCompletions({}, [CM_COMMANDS]), beginCompletions: cmMakeCompletions({}, [this.commands]),
completions: [], completions: [],
nextCompletions: [], nextCompletions: [],
valid: true, valid: true,
@ -397,7 +398,7 @@ define(['core/ArrayUtilities'], (array) => {
return 'comment'; return 'comment';
} }
state.line.push({v: state.current, q: state.currentQuoted}); state.line.push({v: state.current, q: state.currentQuoted});
return cmCheckToken(state, stream.eol()); return cmCheckToken(state, stream.eol(), this.commands);
} }
_tokenEOLFound(stream, state, block) { _tokenEOLFound(stream, state, block) {
@ -406,7 +407,7 @@ define(['core/ArrayUtilities'], (array) => {
return 'comment'; return 'comment';
} }
state.line.push(({v: state.current, q: state.currentQuoted})); state.line.push(({v: state.current, q: state.currentQuoted}));
const type = cmCheckToken(state, false); const type = cmCheckToken(state, false, this.commands);
state.line.pop(); state.line.pop();
return type; return type;
} }

View File

@ -18,19 +18,32 @@ define([
}; };
const CONNECT_TYPES = ((() => { const CONNECT_TYPES = ((() => {
const lTypes = ['', '<', '<<']; const lTypes = [
const mTypes = ['-', '--']; {tok: '', type: 0},
const rTypes = ['', '>', '>>']; {tok: '<', type: 1},
const arrows = array.combine([lTypes, mTypes, rTypes]); {tok: '<<', type: 2},
array.removeAll(arrows, mTypes); ];
const mTypes = [
{tok: '-', type: 'solid'},
{tok: '--', type: 'dash'},
{tok: '~', type: 'wave'},
];
const rTypes = [
{tok: '', type: 0},
{tok: '>', type: 1},
{tok: '>>', type: 2},
];
const arrows = (array.combine([lTypes, mTypes, rTypes])
.filter((arrow) => (arrow[0].type !== 0 || arrow[2].type !== 0))
);
const types = new Map(); const types = new Map();
arrows.forEach((arrow) => { arrows.forEach((arrow) => {
types.set(arrow, { types.set(arrow.map((part) => part.tok).join(''), {
line: arrow.includes('--') ? 'dash' : 'solid', line: arrow[1].type,
left: lTypes.indexOf(arrow.substr(0, arrow.indexOf('-'))), left: arrow[0].type,
right: rTypes.indexOf(arrow.substr(arrow.lastIndexOf('-') + 1)), right: arrow[2].type,
}); });
}); });
@ -411,7 +424,9 @@ define([
return class Parser { return class Parser {
getCodeMirrorMode() { getCodeMirrorMode() {
return SHARED_TOKENISER.getCodeMirrorMode(); return SHARED_TOKENISER.getCodeMirrorMode(
Array.from(CONNECT_TYPES.keys())
);
} }
getCodeMirrorHints() { getCodeMirrorHints() {

View File

@ -195,22 +195,30 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
it('recognises all types of connection', () => { it('recognises all types of connection', () => {
const parsed = parser.parse( const parsed = parser.parse(
'A -> B\n' + 'A->B\n' +
'A <- B\n' + 'A->>B\n' +
'A <-> B\n' + 'A<-B\n' +
'A --> B\n' + 'A<->B\n' +
'A <-- B\n' + 'A<->>B\n' +
'A <--> B\n' + 'A<<-B\n' +
'A ->> B\n' + 'A<<->B\n' +
'A <<- B\n' + 'A<<->>B\n' +
'A <<->> B\n' + 'A-->B\n' +
'A <->> B\n' + 'A-->>B\n' +
'A <<-> B\n' + 'A<--B\n' +
'A -->> B\n' + 'A<-->B\n' +
'A <<-- B\n' + 'A<-->>B\n' +
'A <<-->> B\n' + 'A<<--B\n' +
'A <-->> B\n' + 'A<<-->B\n' +
'A <<--> B\n' 'A<<-->>B\n' +
'A~>B\n' +
'A~>>B\n' +
'A<~B\n' +
'A<~>B\n' +
'A<~>>B\n' +
'A<<~B\n' +
'A<<~>B\n' +
'A<<~>>B\n'
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.connect(['A', 'B'], { PARSED.connect(['A', 'B'], {
@ -219,21 +227,29 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
right: 1, right: 1,
label: '', label: '',
}), }),
PARSED.connect(['A', 'B'], {line: 'solid', left: 0, right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 0}), PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 0}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 1}), PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 1}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 0}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 1}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 1}), PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 1}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 0}), PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 0}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 1}), PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 1}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 0, right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 0}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 1}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 0}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 2}), PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 0}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 1}), PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 1}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 2}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 1}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 2}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 0}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 1}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 2}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 0}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 1}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 2}),
]); ]);
}); });

View File

@ -31,8 +31,8 @@ define(['./CodeMirrorMode'], (CMMode) => {
unescape, unescape,
baseToken: {q: true}, baseToken: {q: true},
}, },
{start: /(?=[^ \t\r\n:+\-*!<>,])/y, end: /(?=[ \t\r\n:+\-*!<>,])|$/y}, {start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
{start: /(?=[\-<>])/y, end: /(?=[^\-<>])|$/y}, {start: /(?=[\-~<>])/y, end: /(?=[^\-~<>])|$/y},
{start: /,/y, baseToken: {v: ','}}, {start: /,/y, baseToken: {v: ','}},
{start: /:/y, baseToken: {v: ':'}}, {start: /:/y, baseToken: {v: ':'}},
{start: /!/y, baseToken: {v: '!'}}, {start: /!/y, baseToken: {v: '!'}},
@ -195,8 +195,8 @@ define(['./CodeMirrorMode'], (CMMode) => {
return tokens; return tokens;
} }
getCodeMirrorMode() { getCodeMirrorMode(arrows) {
return new CMMode(TOKENS); return new CMMode(TOKENS, arrows);
} }
splitLines(tokens) { splitLines(tokens) {

View File

@ -90,6 +90,105 @@ define([
new Arrowhead('double'), new Arrowhead('double'),
]; ];
function makeWavyLineHeights(height) {
return [
0,
-height * 2 / 3,
-height,
-height * 2 / 3,
0,
height * 2 / 3,
height,
height * 2 / 3,
];
}
class ConnectingLine {
renderFlat(container, {x1, x2, y}, attrs) {
const ww = attrs['wave-width'];
const hh = attrs['wave-height'];
if(!ww || !hh) {
container.appendChild(svg.make('line', Object.assign({
'x1': x1,
'y1': y,
'x2': x2,
'y2': y,
}, attrs)));
return;
}
const heights = makeWavyLineHeights(hh);
const dw = ww / heights.length;
let p = 0;
let points = '';
for(let x = x1; x + dw <= x2; x += dw) {
points += (
x + ' ' +
(y + heights[(p ++) % heights.length]) + ' '
);
}
points += x2 + ' ' + y;
container.appendChild(svg.make('polyline', Object.assign({
points,
}, attrs)));
}
renderRev(container, {xL1, xL2, y1, y2, xR}, attrs) {
const r = (y2 - y1) / 2;
const ww = attrs['wave-width'];
const hh = attrs['wave-height'];
if(!ww || !hh) {
container.appendChild(svg.make('path', Object.assign({
'd': (
'M ' + xL1 + ' ' + y1 +
' L ' + xR + ' ' + y1 +
' A ' + r + ' ' + r + ' 0 0 1 ' + xR + ' ' + y2 +
' L ' + xL2 + ' ' + y2
),
}, attrs)));
return;
}
const heights = makeWavyLineHeights(hh);
const dw = ww / heights.length;
let p = 0;
let points = '';
for(let x = xL1; x + dw <= xR; x += dw) {
points += (
x + ' ' +
(y1 + heights[(p ++) % heights.length]) + ' '
);
}
const ym = (y1 + y2) / 2;
for(let t = 0; t + dw / r <= Math.PI; t += dw / r) {
const h = heights[(p ++) % heights.length];
points += (
(xR + Math.sin(t) * (r - h)) + ' ' +
(ym - Math.cos(t) * (r - h)) + ' '
);
}
for(let x = xR; x - dw >= xL2; x -= dw) {
points += (
x + ' ' +
(y2 - heights[(p ++) % heights.length]) + ' '
);
}
points += xL2 + ' ' + y2;
container.appendChild(svg.make('polyline', Object.assign({
points,
}, attrs)));
}
}
const CONNECTING_LINE = new ConnectingLine();
class Connect extends BaseComponent { class Connect extends BaseComponent {
separation({label, agentNames, options}, env) { separation({label, agentNames, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
@ -179,16 +278,13 @@ define([
const y1 = y0 + r * 2; const y1 = y0 + r * 2;
const lineAttrs = config.lineAttrs[options.line]; const lineAttrs = config.lineAttrs[options.line];
env.shapeLayer.appendChild(svg.make('path', Object.assign({ CONNECTING_LINE.renderRev(env.shapeLayer, {
'd': ( xL1: lineX + lArrow.lineGap(env.theme, lineAttrs),
'M ' + (lineX + lArrow.lineGap(env.theme, lineAttrs)) + xL2: lineX + rArrow.lineGap(env.theme, lineAttrs),
' ' + y0 + y1: y0,
' L ' + x1 + ' ' + y0 + y2: y1,
' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 + xR: x1,
' L ' + (lineX + rArrow.lineGap(env.theme, lineAttrs)) + }, lineAttrs);
' ' + y1
),
}, lineAttrs)));
lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, dir: 1}); lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, dir: 1});
rArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y1, dir: 1}); rArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y1, dir: 1});
@ -241,12 +337,11 @@ define([
}); });
const lineAttrs = config.lineAttrs[options.line]; const lineAttrs = config.lineAttrs[options.line];
env.shapeLayer.appendChild(svg.make('line', Object.assign({ CONNECTING_LINE.renderFlat(env.shapeLayer, {
'x1': x0 + lArrow.lineGap(env.theme, lineAttrs) * dir, x1: x0 + lArrow.lineGap(env.theme, lineAttrs) * dir,
'y1': y, x2: x1 - rArrow.lineGap(env.theme, lineAttrs) * dir,
'x2': x1 - rArrow.lineGap(env.theme, lineAttrs) * dir, y,
'y2': y, }, lineAttrs);
}, lineAttrs)));
lArrow.render(env.shapeLayer, env.theme, {x: x0, y, dir}); lArrow.render(env.shapeLayer, env.theme, {x: x0, y, dir});
rArrow.render(env.shapeLayer, env.theme, {x: x1, y, dir: -dir}); rArrow.render(env.shapeLayer, env.theme, {x: x1, y, dir: -dir});

View File

@ -70,6 +70,15 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
'stroke-width': 1, 'stroke-width': 1,
'stroke-dasharray': '4, 2', 'stroke-dasharray': '4, 2',
}, },
'wave': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
'wave-width': 6,
'wave-height': 0.5,
},
}, },
arrow: { arrow: {
single: { single: {

View File

@ -76,6 +76,15 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
'stroke-width': 3, 'stroke-width': 3,
'stroke-dasharray': '10, 4', 'stroke-dasharray': '10, 4',
}, },
'wave': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
'wave-width': 10,
'wave-height': 1,
},
}, },
arrow: { arrow: {
single: { single: {