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 right
Foo <- ]: From the right
[ -> ]: Left to right!
[ ~> ]: Wavy left to right!
# (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];
}
function combineRecur(parts, position, str, target) {
function combineRecur(parts, position, current, target) {
if(position >= parts.length) {
target.push(str);
target.push(current.slice());
return;
}
const choices = parts[position];
if(!Array.isArray(choices)) {
combineRecur(parts, position + 1, str + choices, target);
current.push(choices);
combineRecur(parts, position + 1, current, target);
current.pop();
return;
}
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) {
const target = [];
combineRecur(parts, 0, '', target);
combineRecur(parts, 0, [], target);
return target;
}

View File

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

View File

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

View File

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

View File

@ -195,22 +195,30 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
it('recognises all types of connection', () => {
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([
PARSED.connect(['A', 'B'], {
@ -219,21 +227,29 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
right: 1,
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: 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: 2}),
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: '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: 2, right: 0}),
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,
baseToken: {q: true},
},
{start: /(?=[^ \t\r\n:+\-*!<>,])/y, end: /(?=[ \t\r\n:+\-*!<>,])|$/y},
{start: /(?=[\-<>])/y, end: /(?=[^\-<>])|$/y},
{start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
{start: /(?=[\-~<>])/y, end: /(?=[^\-~<>])|$/y},
{start: /,/y, baseToken: {v: ','}},
{start: /:/y, baseToken: {v: ':'}},
{start: /!/y, baseToken: {v: '!'}},
@ -195,8 +195,8 @@ define(['./CodeMirrorMode'], (CMMode) => {
return tokens;
}
getCodeMirrorMode() {
return new CMMode(TOKENS);
getCodeMirrorMode(arrows) {
return new CMMode(TOKENS, arrows);
}
splitLines(tokens) {

View File

@ -90,6 +90,105 @@ define([
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 {
separation({label, agentNames, options}, env) {
const config = env.theme.connect;
@ -179,16 +278,13 @@ define([
const y1 = y0 + r * 2;
const lineAttrs = config.lineAttrs[options.line];
env.shapeLayer.appendChild(svg.make('path', Object.assign({
'd': (
'M ' + (lineX + lArrow.lineGap(env.theme, lineAttrs)) +
' ' + y0 +
' L ' + x1 + ' ' + y0 +
' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 +
' L ' + (lineX + rArrow.lineGap(env.theme, lineAttrs)) +
' ' + y1
),
}, lineAttrs)));
CONNECTING_LINE.renderRev(env.shapeLayer, {
xL1: lineX + lArrow.lineGap(env.theme, lineAttrs),
xL2: lineX + rArrow.lineGap(env.theme, lineAttrs),
y1: y0,
y2: y1,
xR: x1,
}, lineAttrs);
lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, 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];
env.shapeLayer.appendChild(svg.make('line', Object.assign({
'x1': x0 + lArrow.lineGap(env.theme, lineAttrs) * dir,
'y1': y,
'x2': x1 - rArrow.lineGap(env.theme, lineAttrs) * dir,
'y2': y,
}, lineAttrs)));
CONNECTING_LINE.renderFlat(env.shapeLayer, {
x1: x0 + lArrow.lineGap(env.theme, lineAttrs) * dir,
x2: x1 - rArrow.lineGap(env.theme, lineAttrs) * dir,
y,
}, lineAttrs);
lArrow.render(env.shapeLayer, env.theme, {x: x0, y, 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-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: {
single: {

View File

@ -76,6 +76,15 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
'stroke-width': 3,
'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: {
single: {