Add support for wavy connection lines [#24]
This commit is contained in:
parent
16095cf78a
commit
5b3d0af311
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -196,21 +196,29 @@ 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'
|
||||
);
|
||||
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}),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue