Add support for asynchronous communication [#41]
This commit is contained in:
parent
8344ab2a44
commit
bbb9350e15
22
README.md
22
README.md
|
@ -250,6 +250,28 @@ Foo -> Bar
|
||||||
Bar -> Baz
|
Bar -> Baz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Asynchronous Communication
|
||||||
|
|
||||||
|
<img src="screenshots/AsynchronousCommunication.png" alt="Asynchronous Communication preview" width="200" align="right" />
|
||||||
|
|
||||||
|
```
|
||||||
|
begin Initiator as I, Receiver as R
|
||||||
|
|
||||||
|
# the '...id' syntax allows connections to span multiple lines
|
||||||
|
|
||||||
|
I -> ...fin1
|
||||||
|
...fin1 -> R: FIN
|
||||||
|
|
||||||
|
# they can even inter-mix!
|
||||||
|
R -> ...ack1
|
||||||
|
R -> ...fin2
|
||||||
|
...ack1 -> I: ACK
|
||||||
|
...fin2 -> I: FIN
|
||||||
|
|
||||||
|
!I -> ...ack2
|
||||||
|
...ack2 -> !R: ACK
|
||||||
|
```
|
||||||
|
|
||||||
### Simultaneous Actions (Beta!)
|
### Simultaneous Actions (Beta!)
|
||||||
|
|
||||||
This is a work-in-progress feature. There are situations where this can
|
This is a work-in-progress feature. There are situations where this can
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
19
library.htm
19
library.htm
|
@ -343,6 +343,25 @@ Foo -> Bar
|
||||||
Bar -> Baz
|
Bar -> Baz
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
<h3 id="AsynchronousCommunication">Asynchronous Communication</h3>
|
||||||
|
<pre class="example" data-lang="sequence">
|
||||||
|
begin Initiator as I, Receiver as R
|
||||||
|
|
||||||
|
# the '...id' syntax allows connections to span multiple lines
|
||||||
|
|
||||||
|
I -> ...fin1
|
||||||
|
...fin1 -> R: FIN
|
||||||
|
|
||||||
|
# they can even inter-mix!
|
||||||
|
R -> ...ack1
|
||||||
|
R -> ...fin2
|
||||||
|
...ack1 -> I: ACK
|
||||||
|
...fin2 -> I: FIN
|
||||||
|
|
||||||
|
!I -> ...ack2
|
||||||
|
...ack2 -> !R: ACK
|
||||||
|
</pre>
|
||||||
|
|
||||||
<h3 id="LanguageMore">More</h3>
|
<h3 id="LanguageMore">More</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -56,6 +56,15 @@
|
||||||
title: 'Self-connection',
|
title: 'Self-connection',
|
||||||
code: '{Agent1} -> {Agent1}: {Message}',
|
code: '{Agent1} -> {Agent1}: {Message}',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Asynchronous message',
|
||||||
|
code: '{Agent1} -> ...{id}\n...{id} -> {Agent2}: {Message}',
|
||||||
|
preview: (
|
||||||
|
'begin A, B\n' +
|
||||||
|
'A -> ...x\n' +
|
||||||
|
'...x -> B: Message'
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Found message',
|
title: 'Found message',
|
||||||
code: '* -> {Agent1}: {Message}',
|
code: '* -> {Agent1}: {Message}',
|
||||||
|
|
|
@ -2,8 +2,11 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const TRIMMER = /^([ \t]*)(.*)$/;
|
const TRIMMER = /^([ \t]*)(.*)$/;
|
||||||
const SQUASH_START = /^[ \t\r\n:,]/;
|
const SQUASH = {
|
||||||
const SQUASH_END = /[ \t\r\n]$/;
|
start: /^[ \t\r\n:,]/,
|
||||||
|
end: /[ \t\r\n]$/,
|
||||||
|
after: '.!+', // cannot squash after * or - in all cases
|
||||||
|
};
|
||||||
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
|
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
|
||||||
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
|
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
|
||||||
const QUOTE_ESCAPE = /["\\]/g;
|
const QUOTE_ESCAPE = /["\\]/g;
|
||||||
|
@ -24,6 +27,9 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
squash: {line: line, ch: chFrom},
|
squash: {line: line, ch: chFrom},
|
||||||
};
|
};
|
||||||
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
|
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
|
||||||
|
if(SQUASH.after.includes(ln[chFrom - 2])) {
|
||||||
|
ranges.word.ch --;
|
||||||
|
}
|
||||||
ranges.squash.ch --;
|
ranges.squash.ch --;
|
||||||
}
|
}
|
||||||
return ranges;
|
return ranges;
|
||||||
|
@ -69,8 +75,8 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
text: quoted,
|
text: quoted,
|
||||||
displayText: quoted.trim(),
|
displayText: quoted.trim(),
|
||||||
className: null,
|
className: null,
|
||||||
from: SQUASH_START.test(quoted) ? from.squash : from.word,
|
from: SQUASH.start.test(quoted) ? from.squash : from.word,
|
||||||
to: SQUASH_END.test(quoted) ? ranges.to.squash : ranges.to.word,
|
to: SQUASH.end.test(quoted) ? ranges.to.squash : ranges.to.word,
|
||||||
displayFrom: from.word,
|
displayFrom: from.word,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,24 +205,67 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
const connect = {
|
const connect = {
|
||||||
type: 'keyword',
|
type: 'keyword',
|
||||||
suggest: true,
|
suggest: true,
|
||||||
then: makeOpBlock(agentToOptText, {
|
then: Object.assign({}, makeOpBlock(agentToOptText, {
|
||||||
':': colonTextToEnd,
|
':': colonTextToEnd,
|
||||||
'\n': hiddenEnd,
|
'\n': hiddenEnd,
|
||||||
|
}), {
|
||||||
|
'...': {type: 'operator', suggest: true, then: {
|
||||||
|
'': {
|
||||||
|
type: 'variable',
|
||||||
|
suggest: {known: 'DelayedAgent'},
|
||||||
|
then: {
|
||||||
|
'': 0,
|
||||||
|
':': CM_ERROR,
|
||||||
|
'\n': end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const then = {'': 0};
|
const connectors = {};
|
||||||
arrows.forEach((arrow) => (then[arrow] = connect));
|
arrows.forEach((arrow) => (connectors[arrow] = connect));
|
||||||
then[':'] = {
|
|
||||||
|
const labelIndicator = {
|
||||||
type: 'operator',
|
type: 'operator',
|
||||||
suggest: true,
|
suggest: true,
|
||||||
override: 'Label',
|
override: 'Label',
|
||||||
then: {},
|
then: {},
|
||||||
};
|
};
|
||||||
return makeOpBlock(
|
|
||||||
{type: 'variable', suggest: {known: 'Agent'}, then},
|
const hiddenLabelIndicator = {
|
||||||
then
|
type: 'operator',
|
||||||
);
|
suggest: false,
|
||||||
|
override: 'Label',
|
||||||
|
then: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstAgent = {
|
||||||
|
type: 'variable',
|
||||||
|
suggest: {known: 'Agent'},
|
||||||
|
then: Object.assign({
|
||||||
|
'': 0,
|
||||||
|
':': labelIndicator,
|
||||||
|
}, connectors),
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstAgentDelayed = {
|
||||||
|
type: 'variable',
|
||||||
|
suggest: {known: 'DelayedAgent'},
|
||||||
|
then: Object.assign({
|
||||||
|
'': 0,
|
||||||
|
':': hiddenLabelIndicator,
|
||||||
|
}, connectors),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.assign({
|
||||||
|
'...': {type: 'operator', suggest: true, then: {
|
||||||
|
'': firstAgentDelayed,
|
||||||
|
}},
|
||||||
|
}, makeOpBlock(firstAgent, Object.assign({
|
||||||
|
'': firstAgent,
|
||||||
|
':': hiddenLabelIndicator,
|
||||||
|
}, connectors)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_THEN = {
|
const BASE_THEN = {
|
||||||
|
@ -480,6 +523,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
currentSpace: '',
|
currentSpace: '',
|
||||||
currentQuoted: false,
|
currentQuoted: false,
|
||||||
knownAgent: [],
|
knownAgent: [],
|
||||||
|
knownDelayedAgent: [],
|
||||||
knownLabel: [],
|
knownLabel: [],
|
||||||
beginCompletions: cmMakeCompletions({}, [this.commands]),
|
beginCompletions: cmMakeCompletions({}, [this.commands]),
|
||||||
completions: [],
|
completions: [],
|
||||||
|
|
|
@ -139,7 +139,7 @@ defineDescribe('Code Mirror Mode', [
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recognises agent flags', () => {
|
it('recognises agent flags on the right', () => {
|
||||||
cm.getDoc().setValue('Foo -> *Bar');
|
cm.getDoc().setValue('Foo -> *Bar');
|
||||||
expect(getTokens(0)).toEqual([
|
expect(getTokens(0)).toEqual([
|
||||||
{v: 'Foo', type: 'variable'},
|
{v: 'Foo', type: 'variable'},
|
||||||
|
@ -149,6 +149,16 @@ defineDescribe('Code Mirror Mode', [
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognises agent flags on the left', () => {
|
||||||
|
cm.getDoc().setValue('*Foo -> Bar');
|
||||||
|
expect(getTokens(0)).toEqual([
|
||||||
|
{v: '*', type: 'operator'},
|
||||||
|
{v: 'Foo', type: 'variable'},
|
||||||
|
{v: ' ->', type: 'keyword'},
|
||||||
|
{v: ' Bar', type: 'variable'},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects missing agent names', () => {
|
it('rejects missing agent names', () => {
|
||||||
cm.getDoc().setValue('+ -> Bar');
|
cm.getDoc().setValue('+ -> Bar');
|
||||||
expect(getTokens(0)[2].type).toContain('line-error');
|
expect(getTokens(0)[2].type).toContain('line-error');
|
||||||
|
@ -198,6 +208,30 @@ defineDescribe('Code Mirror Mode', [
|
||||||
expect(getTokens(0)[4].type).toContain('line-error');
|
expect(getTokens(0)[4].type).toContain('line-error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('highlights delayed message syntax', () => {
|
||||||
|
cm.getDoc().setValue('A -> ...x\n...x -> B: hello');
|
||||||
|
expect(getTokens(0)).toEqual([
|
||||||
|
{v: 'A', type: 'variable'},
|
||||||
|
{v: ' ->', type: 'keyword'},
|
||||||
|
{v: ' ...', type: 'operator'},
|
||||||
|
{v: 'x', type: 'variable'},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(getTokens(1)).toEqual([
|
||||||
|
{v: '...', type: 'operator'},
|
||||||
|
{v: 'x', type: 'variable'},
|
||||||
|
{v: ' ->', type: 'keyword'},
|
||||||
|
{v: ' B', type: 'variable'},
|
||||||
|
{v: ':', type: 'operator'},
|
||||||
|
{v: ' hello', type: 'string'},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recognises invalid delayed messages', () => {
|
||||||
|
cm.getDoc().setValue('A -> ...x: hello');
|
||||||
|
expect(getTokens(0)[4].type).toContain('line-error');
|
||||||
|
});
|
||||||
|
|
||||||
it('highlights block statements', () => {
|
it('highlights block statements', () => {
|
||||||
cm.getDoc().setValue(
|
cm.getDoc().setValue(
|
||||||
'if\n' +
|
'if\n' +
|
||||||
|
@ -491,6 +525,7 @@ defineDescribe('Code Mirror Mode', [
|
||||||
'* ',
|
'* ',
|
||||||
'! ',
|
'! ',
|
||||||
'Foo ',
|
'Foo ',
|
||||||
|
'... ',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -572,5 +607,10 @@ defineDescribe('Code Mirror Mode', [
|
||||||
expect(hints).not.toContain('"Zig -> Zag" ');
|
expect(hints).not.toContain('"Zig -> Zag" ');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('suggests known delayed agents', () => {
|
||||||
|
cm.getDoc().setValue('A -> ...woo\n... ');
|
||||||
|
const hints = getHintTexts({line: 1, ch: 4});
|
||||||
|
expect(hints).toEqual(['woo ']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -256,6 +256,8 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
'divider': this.handleDivider.bind(this),
|
'divider': this.handleDivider.bind(this),
|
||||||
'label pattern': this.handleLabelPattern.bind(this),
|
'label pattern': this.handleLabelPattern.bind(this),
|
||||||
'connect': this.handleConnect.bind(this),
|
'connect': this.handleConnect.bind(this),
|
||||||
|
'connect-delay-begin': this.handleConnectDelayBegin.bind(this),
|
||||||
|
'connect-delay-end': this.handleConnectDelayEnd.bind(this),
|
||||||
'note over': this.handleNote.bind(this),
|
'note over': this.handleNote.bind(this),
|
||||||
'note left': this.handleNote.bind(this),
|
'note left': this.handleNote.bind(this),
|
||||||
'note right': this.handleNote.bind(this),
|
'note right': this.handleNote.bind(this),
|
||||||
|
@ -436,13 +438,31 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_makeSection(header, stages) {
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
delayedConnections: new Map(),
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkSectionEnd() {
|
||||||
|
const dcs = this.currentSection.delayedConnections;
|
||||||
|
if(dcs.size > 0) {
|
||||||
|
const dc = dcs.values().next().value;
|
||||||
|
throw new Error(
|
||||||
|
'Unused delayed connection "' + dc.tag +
|
||||||
|
'" at line ' + (dc.ln + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beginNested(blockType, {tag, label, name, ln}) {
|
beginNested(blockType, {tag, label, name, ln}) {
|
||||||
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
|
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
|
||||||
const rightGAgent = GAgent.make(name + ']');
|
const rightGAgent = GAgent.make(name + ']');
|
||||||
const gAgents = [leftGAgent, rightGAgent];
|
const gAgents = [leftGAgent, rightGAgent];
|
||||||
const stages = [];
|
const stages = [];
|
||||||
this.currentSection = {
|
this.currentSection = this._makeSection({
|
||||||
header: {
|
|
||||||
type: 'block begin',
|
type: 'block begin',
|
||||||
blockType,
|
blockType,
|
||||||
tag: this.textFormatter(tag),
|
tag: this.textFormatter(tag),
|
||||||
|
@ -450,9 +470,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
left: leftGAgent.id,
|
left: leftGAgent.id,
|
||||||
right: rightGAgent.id,
|
right: rightGAgent.id,
|
||||||
ln,
|
ln,
|
||||||
},
|
}, stages);
|
||||||
stages,
|
|
||||||
};
|
|
||||||
this.currentNest = {
|
this.currentNest = {
|
||||||
blockType,
|
blockType,
|
||||||
gAgents,
|
gAgents,
|
||||||
|
@ -496,8 +514,8 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
this.currentNest.blockType + ')'
|
this.currentNest.blockType + ')'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.currentSection = {
|
this._checkSectionEnd();
|
||||||
header: {
|
this.currentSection = this._makeSection({
|
||||||
type: 'block split',
|
type: 'block split',
|
||||||
blockType,
|
blockType,
|
||||||
tag: this.textFormatter(tag),
|
tag: this.textFormatter(tag),
|
||||||
|
@ -505,9 +523,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
left: this.currentNest.leftGAgent.id,
|
left: this.currentNest.leftGAgent.id,
|
||||||
right: this.currentNest.rightGAgent.id,
|
right: this.currentNest.rightGAgent.id,
|
||||||
ln,
|
ln,
|
||||||
},
|
}, []);
|
||||||
stages: [],
|
|
||||||
};
|
|
||||||
this.currentNest.sections.push(this.currentSection);
|
this.currentNest.sections.push(this.currentSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -515,6 +531,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
if(this.nesting.length <= 1) {
|
if(this.nesting.length <= 1) {
|
||||||
throw new Error('Invalid block nesting (too many "end"s)');
|
throw new Error('Invalid block nesting (too many "end"s)');
|
||||||
}
|
}
|
||||||
|
this._checkSectionEnd();
|
||||||
const nested = this.nesting.pop();
|
const nested = this.nesting.pop();
|
||||||
this.currentNest = array.last(this.nesting);
|
this.currentNest = array.last(this.nesting);
|
||||||
this.currentSection = array.last(this.currentNest.sections);
|
this.currentSection = array.last(this.currentNest.sections);
|
||||||
|
@ -793,24 +810,20 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
return gAgents;
|
return gAgents;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnect({agents, label, options}) {
|
_handlePartialConnect(agents) {
|
||||||
const flags = this.filterConnectFlags(agents);
|
const flags = this.filterConnectFlags(agents);
|
||||||
|
|
||||||
let gAgents = agents.map(this.toGAgent);
|
const gAgents = agents.map(this.toGAgent);
|
||||||
this.validateGAgents(gAgents, {
|
this.validateGAgents(gAgents, {
|
||||||
allowGrouped: true,
|
allowGrouped: true,
|
||||||
allowVirtual: true,
|
allowVirtual: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
|
this.defineGAgents(array
|
||||||
this.defineGAgents(allGAgents
|
.flatMap(gAgents, this.expandGroupedGAgent)
|
||||||
.filter((gAgent) => !gAgent.isVirtualSource)
|
.filter((gAgent) => !gAgent.isVirtualSource)
|
||||||
);
|
);
|
||||||
|
|
||||||
gAgents = this.expandGroupedGAgentConnection(gAgents);
|
|
||||||
gAgents = this.expandVirtualSourceAgents(gAgents);
|
|
||||||
const agentIDs = gAgents.map((gAgent) => gAgent.id);
|
|
||||||
|
|
||||||
const implicitBeginGAgents = (agents
|
const implicitBeginGAgents = (agents
|
||||||
.filter(PAgent.hasFlag('begin', false))
|
.filter(PAgent.hasFlag('begin', false))
|
||||||
.map(this.toGAgent)
|
.map(this.toGAgent)
|
||||||
|
@ -818,20 +831,105 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
);
|
);
|
||||||
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
|
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
|
||||||
|
|
||||||
const connectStage = {
|
return {flags, gAgents};
|
||||||
type: 'connect',
|
}
|
||||||
agentIDs,
|
|
||||||
label: this.textFormatter(this.applyLabelPattern(label)),
|
|
||||||
options,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addParallelStages([
|
_makeConnectParallelStages(flags, connectStage) {
|
||||||
|
return [
|
||||||
this.setGAgentVis(flags.beginGAgents, true, 'box', true),
|
this.setGAgentVis(flags.beginGAgents, true, 'box', true),
|
||||||
this.setGAgentHighlight(flags.startGAgents, true, true),
|
this.setGAgentHighlight(flags.startGAgents, true, true),
|
||||||
connectStage,
|
connectStage,
|
||||||
this.setGAgentHighlight(flags.stopGAgents, false, true),
|
this.setGAgentHighlight(flags.stopGAgents, false, true),
|
||||||
this.setGAgentVis(flags.endGAgents, false, 'cross', true),
|
this.setGAgentVis(flags.endGAgents, false, 'cross', true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnect({agents, label, options}) {
|
||||||
|
let {flags, gAgents} = this._handlePartialConnect(agents);
|
||||||
|
|
||||||
|
gAgents = this.expandGroupedGAgentConnection(gAgents);
|
||||||
|
gAgents = this.expandVirtualSourceAgents(gAgents);
|
||||||
|
|
||||||
|
const connectStage = {
|
||||||
|
type: 'connect',
|
||||||
|
agentIDs: gAgents.map((gAgent) => gAgent.id),
|
||||||
|
label: this.textFormatter(this.applyLabelPattern(label)),
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addParallelStages(this._makeConnectParallelStages(
|
||||||
|
flags,
|
||||||
|
connectStage
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnectDelayBegin({agent, tag, options, ln}) {
|
||||||
|
const dcs = this.currentSection.delayedConnections;
|
||||||
|
if(dcs.has(tag)) {
|
||||||
|
throw new Error('Duplicate delayed connection "' + tag + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {flags, gAgents} = this._handlePartialConnect([agent]);
|
||||||
|
const uniqueTag = this.nextVirtualAgentName();
|
||||||
|
|
||||||
|
const connectStage = {
|
||||||
|
type: 'connect-delay-begin',
|
||||||
|
tag: uniqueTag,
|
||||||
|
agentIDs: null,
|
||||||
|
label: null,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
dcs.set(tag, {tag, uniqueTag, ln, gAgents, connectStage});
|
||||||
|
|
||||||
|
this.addParallelStages(this._makeConnectParallelStages(
|
||||||
|
flags,
|
||||||
|
connectStage
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnectDelayEnd({agent, tag, label, options}) {
|
||||||
|
const dcs = this.currentSection.delayedConnections;
|
||||||
|
const dcInfo = dcs.get(tag);
|
||||||
|
if(!dcInfo) {
|
||||||
|
throw new Error('Unknown delayed connection "' + tag + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
let {flags, gAgents} = this._handlePartialConnect([agent]);
|
||||||
|
|
||||||
|
gAgents = this.expandGroupedGAgentConnection([
|
||||||
|
...dcInfo.gAgents,
|
||||||
|
...gAgents,
|
||||||
]);
|
]);
|
||||||
|
gAgents = this.expandVirtualSourceAgents(gAgents);
|
||||||
|
|
||||||
|
let combinedOptions = dcInfo.connectStage.options;
|
||||||
|
if(combinedOptions.line !== options.line) {
|
||||||
|
throw new Error('Mismatched delayed connection arrows');
|
||||||
|
}
|
||||||
|
if(options.right) {
|
||||||
|
combinedOptions = Object.assign({}, combinedOptions, {
|
||||||
|
right: options.right,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(dcInfo.connectStage, {
|
||||||
|
agentIDs: gAgents.map((gAgent) => gAgent.id),
|
||||||
|
label: this.textFormatter(this.applyLabelPattern(label)),
|
||||||
|
options: combinedOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectEndStage = {
|
||||||
|
type: 'connect-delay-end',
|
||||||
|
tag: dcInfo.uniqueTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addParallelStages(this._makeConnectParallelStages(
|
||||||
|
flags,
|
||||||
|
connectEndStage
|
||||||
|
));
|
||||||
|
|
||||||
|
dcs.delete(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNote({type, agents, mode, label}) {
|
handleNote({type, agents, mode, label}) {
|
||||||
|
@ -952,6 +1050,8 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
throw new Error('Unterminated group');
|
throw new Error('Unterminated group');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._checkSectionEnd();
|
||||||
|
|
||||||
const terminators = meta.terminators || 'none';
|
const terminators = meta.terminators || 'none';
|
||||||
this.addParallelStages([
|
this.addParallelStages([
|
||||||
this.setGAgentHighlight(this.gAgents, false),
|
this.setGAgentHighlight(this.gAgents, false),
|
||||||
|
|
|
@ -94,6 +94,49 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
connectDelayBegin: (agentID, {
|
||||||
|
label = '',
|
||||||
|
tag = '',
|
||||||
|
line = '',
|
||||||
|
left = 0,
|
||||||
|
right = 0,
|
||||||
|
ln = 0,
|
||||||
|
} = {}) => {
|
||||||
|
return {
|
||||||
|
type: 'connect-delay-begin',
|
||||||
|
ln,
|
||||||
|
tag,
|
||||||
|
agent: makeParsedAgents([agentID])[0],
|
||||||
|
options: {
|
||||||
|
line,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
connectDelayEnd: (agentID, {
|
||||||
|
label = '',
|
||||||
|
tag = '',
|
||||||
|
line = '',
|
||||||
|
left = 0,
|
||||||
|
right = 0,
|
||||||
|
ln = 0,
|
||||||
|
} = {}) => {
|
||||||
|
return {
|
||||||
|
type: 'connect-delay-end',
|
||||||
|
ln,
|
||||||
|
tag,
|
||||||
|
agent: makeParsedAgents([agentID])[0],
|
||||||
|
label,
|
||||||
|
options: {
|
||||||
|
line,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
note: (type, agentIDs, {
|
note: (type, agentIDs, {
|
||||||
mode = '',
|
mode = '',
|
||||||
label = '',
|
label = '',
|
||||||
|
@ -211,6 +254,39 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
connectDelayBegin: (agentIDs, {
|
||||||
|
label = any(),
|
||||||
|
tag = any(),
|
||||||
|
line = any(),
|
||||||
|
left = any(),
|
||||||
|
right = any(),
|
||||||
|
ln = any(),
|
||||||
|
} = {}) => {
|
||||||
|
return {
|
||||||
|
type: 'connect-delay-begin',
|
||||||
|
agentIDs,
|
||||||
|
label,
|
||||||
|
tag,
|
||||||
|
options: {
|
||||||
|
line,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
},
|
||||||
|
ln,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
connectDelayEnd: ({
|
||||||
|
tag = any(),
|
||||||
|
ln = any(),
|
||||||
|
} = {}) => {
|
||||||
|
return {
|
||||||
|
type: 'connect-delay-end',
|
||||||
|
tag,
|
||||||
|
ln,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
highlight: (agentIDs, highlighted, {
|
highlight: (agentIDs, highlighted, {
|
||||||
ln = any(),
|
ln = any(),
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
|
@ -607,6 +683,183 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('aggregates delayed connect information in the first entry', () => {
|
||||||
|
const sequence = invoke([
|
||||||
|
PARSED.beginAgents(['A', 'B']),
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
ln: 0,
|
||||||
|
tag: 'foo',
|
||||||
|
line: 'solid',
|
||||||
|
left: 0,
|
||||||
|
right: 1,
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 1,
|
||||||
|
tag: 'foo',
|
||||||
|
label: 'woo',
|
||||||
|
line: 'solid',
|
||||||
|
left: 0,
|
||||||
|
right: 1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(sequence.stages).toEqual([
|
||||||
|
any(),
|
||||||
|
GENERATED.connectDelayBegin(['A', 'B'], {
|
||||||
|
label: 'woo!',
|
||||||
|
tag: '__0',
|
||||||
|
line: 'solid',
|
||||||
|
left: 0,
|
||||||
|
right: 1,
|
||||||
|
ln: 0,
|
||||||
|
}),
|
||||||
|
GENERATED.connectDelayEnd({
|
||||||
|
tag: '__0',
|
||||||
|
ln: 1,
|
||||||
|
}),
|
||||||
|
any(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges delayed connect arrows', () => {
|
||||||
|
const sequence = invoke([
|
||||||
|
PARSED.beginAgents(['A', 'B']),
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
tag: 'foo',
|
||||||
|
line: 'solid',
|
||||||
|
left: 1,
|
||||||
|
right: 0,
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
tag: 'foo',
|
||||||
|
line: 'solid',
|
||||||
|
left: 0,
|
||||||
|
right: 1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(sequence.stages).toEqual([
|
||||||
|
any(),
|
||||||
|
GENERATED.connectDelayBegin(['A', 'B'], {
|
||||||
|
line: 'solid',
|
||||||
|
left: 1,
|
||||||
|
right: 1,
|
||||||
|
}),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects conflicting delayed message arrows', () => {
|
||||||
|
expect(() => invoke([
|
||||||
|
PARSED.beginAgents(['A', 'B']),
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
tag: 'foo',
|
||||||
|
line: 'abc',
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 1,
|
||||||
|
tag: 'foo',
|
||||||
|
line: 'def',
|
||||||
|
}),
|
||||||
|
])).toThrow(new Error(
|
||||||
|
'Mismatched delayed connection arrows at line 2'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('implicitly begins agents in delayed connections', () => {
|
||||||
|
const sequence = invoke([
|
||||||
|
PARSED.connectDelayBegin('A', {tag: 'foo'}),
|
||||||
|
PARSED.connectDelayEnd('B', {tag: 'foo'}),
|
||||||
|
]);
|
||||||
|
expect(sequence.stages).toEqual([
|
||||||
|
GENERATED.beginAgents(['A']),
|
||||||
|
GENERATED.connectDelayBegin(['A', 'B']),
|
||||||
|
GENERATED.beginAgents(['B']),
|
||||||
|
GENERATED.connectDelayEnd(),
|
||||||
|
GENERATED.endAgents(['A', 'B']),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unknown delayed connections', () => {
|
||||||
|
expect(() => invoke([
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
ln: 0,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 1,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 2,
|
||||||
|
tag: 'bar',
|
||||||
|
}),
|
||||||
|
])).toThrow(new Error(
|
||||||
|
'Unknown delayed connection "bar" at line 3'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects overused delayed connections', () => {
|
||||||
|
expect(() => invoke([
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
ln: 0,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 1,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 2,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
])).toThrow(new Error(
|
||||||
|
'Unknown delayed connection "foo" at line 3'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unused delayed connections', () => {
|
||||||
|
expect(() => invoke([
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
ln: 0,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
])).toThrow(new Error(
|
||||||
|
'Unused delayed connection "foo" at line 1'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects duplicate delayed connection names', () => {
|
||||||
|
expect(() => invoke([
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
ln: 0,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.connectDelayBegin('B', {
|
||||||
|
ln: 1,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
])).toThrow(new Error(
|
||||||
|
'Duplicate delayed connection "foo" at line 2'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects delayed connections passing block boundaries', () => {
|
||||||
|
expect(() => invoke([
|
||||||
|
PARSED.connectDelayBegin('A', {
|
||||||
|
ln: 0,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.blockBegin('if', ''),
|
||||||
|
PARSED.connectDelayEnd('B', {
|
||||||
|
ln: 1,
|
||||||
|
tag: 'foo',
|
||||||
|
}),
|
||||||
|
PARSED.blockEnd(),
|
||||||
|
])).toThrow(new Error(
|
||||||
|
'Unknown delayed connection "foo" at line 2'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
it('creates implicit end stages for all remaining agents', () => {
|
it('creates implicit end stages for all remaining agents', () => {
|
||||||
const sequence = invoke([
|
const sequence = invoke([
|
||||||
PARSED.connect(['A', 'B']),
|
PARSED.connect(['A', 'B']),
|
||||||
|
|
|
@ -132,17 +132,6 @@ define([
|
||||||
return new Error(message + suffix);
|
return new Error(message + suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
function errToken(line, pos) {
|
|
||||||
if(pos < line.length) {
|
|
||||||
return line[pos];
|
|
||||||
}
|
|
||||||
const last = array.last(line);
|
|
||||||
if(!last) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {b: last.e};
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinLabel(line, begin = 0, end = null) {
|
function joinLabel(line, begin = 0, end = null) {
|
||||||
if(end === null) {
|
if(end === null) {
|
||||||
end = line.length;
|
end = line.length;
|
||||||
|
@ -206,6 +195,19 @@ define([
|
||||||
return orEnd ? limit : -1;
|
return orEnd ? limit : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findFirstToken(line, tokenMap, {start = 0, limit = null} = {}) {
|
||||||
|
if(limit === null) {
|
||||||
|
limit = line.length;
|
||||||
|
}
|
||||||
|
for(let pos = start; pos < limit; ++ pos) {
|
||||||
|
const value = tokenMap.get(tokenKeyword(line[pos]));
|
||||||
|
if(value) {
|
||||||
|
return {pos, value};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
|
function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
|
||||||
let aliasSep = -1;
|
let aliasSep = -1;
|
||||||
if(enableAlias) {
|
if(enableAlias) {
|
||||||
|
@ -215,7 +217,11 @@ define([
|
||||||
aliasSep = end;
|
aliasSep = end;
|
||||||
}
|
}
|
||||||
if(start >= aliasSep && !allowBlankName) {
|
if(start >= aliasSep && !allowBlankName) {
|
||||||
throw makeError('Missing agent name', errToken(line, start));
|
let errPosToken = line[start];
|
||||||
|
if(!errPosToken) {
|
||||||
|
errPosToken = {b: array.last(line).e};
|
||||||
|
}
|
||||||
|
throw makeError('Missing agent name', errPosToken);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: joinLabel(line, start, aliasSep),
|
name: joinLabel(line, start, aliasSep),
|
||||||
|
@ -491,35 +497,54 @@ define([
|
||||||
},
|
},
|
||||||
|
|
||||||
(line) => { // connect
|
(line) => { // connect
|
||||||
let labelSep = findToken(line, ':');
|
const labelSep = findToken(line, ':', {orEnd: true});
|
||||||
if(labelSep === -1) {
|
const connectionToken = findFirstToken(
|
||||||
labelSep = line.length;
|
line,
|
||||||
}
|
CONNECT.types,
|
||||||
let typePos = -1;
|
{start: 0, limit: labelSep - 1}
|
||||||
let options = null;
|
);
|
||||||
for(let j = 0; j < line.length; ++ j) {
|
if(!connectionToken) {
|
||||||
const opts = CONNECT.types.get(tokenKeyword(line[j]));
|
|
||||||
if(opts) {
|
|
||||||
typePos = j;
|
|
||||||
options = opts;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(typePos <= 0 || typePos >= labelSep - 1) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const readAgentOpts = {
|
|
||||||
|
const connectPos = connectionToken.pos;
|
||||||
|
|
||||||
|
const readOpts = {
|
||||||
flagTypes: CONNECT.agentFlags,
|
flagTypes: CONNECT.agentFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(tokenKeyword(line[0]) === '...') {
|
||||||
|
return {
|
||||||
|
type: 'connect-delay-end',
|
||||||
|
tag: joinLabel(line, 1, connectPos),
|
||||||
|
agent: readAgent(line, connectPos + 1, labelSep, readOpts),
|
||||||
|
label: joinLabel(line, labelSep + 1),
|
||||||
|
options: connectionToken.value,
|
||||||
|
};
|
||||||
|
} else if(tokenKeyword(line[connectPos + 1]) === '...') {
|
||||||
|
if(labelSep !== line.length) {
|
||||||
|
throw makeError(
|
||||||
|
'Cannot label beginning of delayed connection',
|
||||||
|
line[labelSep]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'connect-delay-begin',
|
||||||
|
tag: joinLabel(line, connectPos + 2, labelSep),
|
||||||
|
agent: readAgent(line, 0, connectPos, readOpts),
|
||||||
|
options: connectionToken.value,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
return {
|
return {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
agents: [
|
agents: [
|
||||||
readAgent(line, 0, typePos, readAgentOpts),
|
readAgent(line, 0, connectPos, readOpts),
|
||||||
readAgent(line, typePos + 1, labelSep, readAgentOpts),
|
readAgent(line, connectPos + 1, labelSep, readOpts),
|
||||||
],
|
],
|
||||||
label: joinLabel(line, labelSep + 1),
|
label: joinLabel(line, labelSep + 1),
|
||||||
options,
|
options: connectionToken.value,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
(line) => { // marker
|
(line) => { // marker
|
||||||
|
|
|
@ -328,6 +328,43 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('converts delayed connections', () => {
|
||||||
|
const parsed = parser.parse('+A <- ...foo\n...foo -> -B: woo');
|
||||||
|
expect(parsed.stages).toEqual([
|
||||||
|
{
|
||||||
|
type: 'connect-delay-begin',
|
||||||
|
ln: jasmine.anything(),
|
||||||
|
tag: 'foo',
|
||||||
|
agent: {
|
||||||
|
name: 'A',
|
||||||
|
alias: '',
|
||||||
|
flags: ['start'],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
line: 'solid',
|
||||||
|
left: 1,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'connect-delay-end',
|
||||||
|
ln: jasmine.anything(),
|
||||||
|
tag: 'foo',
|
||||||
|
agent: {
|
||||||
|
name: 'B',
|
||||||
|
alias: '',
|
||||||
|
flags: ['stop'],
|
||||||
|
},
|
||||||
|
label: 'woo',
|
||||||
|
options: {
|
||||||
|
line: 'solid',
|
||||||
|
left: 0,
|
||||||
|
right: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('converts notes', () => {
|
it('converts notes', () => {
|
||||||
const parsed = parser.parse('note over A: hello there');
|
const parsed = parser.parse('note over A: hello there');
|
||||||
expect(parsed.stages).toEqual([{
|
expect(parsed.stages).toEqual([{
|
||||||
|
@ -722,6 +759,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
||||||
expect(() => parser.parse('A -> : hello')).toThrow();
|
expect(() => parser.parse('A -> : hello')).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects messages on delayed connections', () => {
|
||||||
|
expect(() => parser.parse('A -> ...a: nope')).toThrow(new Error(
|
||||||
|
'Cannot label beginning of delayed connection' +
|
||||||
|
' at line 1, character 9'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects invalid terminators', () => {
|
it('rejects invalid terminators', () => {
|
||||||
expect(() => parser.parse('terminators foo')).toThrow(new Error(
|
expect(() => parser.parse('terminators foo')).toThrow(new Error(
|
||||||
'Unknown termination "foo" at line 1, character 12'
|
'Unknown termination "foo" at line 1, character 12'
|
||||||
|
|
|
@ -121,7 +121,7 @@ defineDescribe('SequenceDiagram', [
|
||||||
'<path d="M20.5 26L48.5 26"'
|
'<path d="M20.5 26L48.5 26"'
|
||||||
);
|
);
|
||||||
expect(content).toContain(
|
expect(content).toContain(
|
||||||
'<polygon points="46 21 51 26 46 31"'
|
'<polygon points="46 31 51 26 46 21"'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ define(['./CodeMirrorMode'], (CMMode) => {
|
||||||
escapeWith: unescape,
|
escapeWith: unescape,
|
||||||
baseToken: {q: true},
|
baseToken: {q: true},
|
||||||
},
|
},
|
||||||
|
{start: /\.\.\./y, baseToken: {v: '...'}},
|
||||||
{start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
|
{start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
|
||||||
{
|
{
|
||||||
start: /(?=[\-~<])/y,
|
start: /(?=[\-~<])/y,
|
||||||
|
|
|
@ -69,6 +69,20 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses special characters as tokens', () => {
|
||||||
|
const input = ',:!+*...abc';
|
||||||
|
const tokens = tokeniser.tokenise(input);
|
||||||
|
expect(tokens).toEqual([
|
||||||
|
token({s: '', v: ','}),
|
||||||
|
token({s: '', v: ':'}),
|
||||||
|
token({s: '', v: '!'}),
|
||||||
|
token({s: '', v: '+'}),
|
||||||
|
token({s: '', v: '*'}),
|
||||||
|
token({s: '', v: '...'}),
|
||||||
|
token({s: '', v: 'abc'}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('stores line numbers', () => {
|
it('stores line numbers', () => {
|
||||||
const input = 'foo bar\nbaz';
|
const input = 'foo bar\nbaz';
|
||||||
const tokens = tokeniser.tokenise(input);
|
const tokens = tokeniser.tokenise(input);
|
||||||
|
|
|
@ -57,6 +57,7 @@ define([
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}), clickable.firstChild);
|
}), clickable.firstChild);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -94,6 +95,7 @@ define([
|
||||||
'width': d * 2,
|
'width': d * 2,
|
||||||
'height': d * 2,
|
'height': d * 2,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -148,6 +150,7 @@ define([
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -208,6 +211,7 @@ define([
|
||||||
'width': config.width,
|
'width': config.width,
|
||||||
'height': config.height,
|
'height': config.height,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -242,6 +246,7 @@ define([
|
||||||
'width': w,
|
'width': w,
|
||||||
'height': config.height,
|
'height': config.height,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -77,6 +77,7 @@ define([
|
||||||
'width': agentInfoR.x - agentInfoL.x,
|
'width': agentInfoR.x - agentInfoL.x,
|
||||||
'height': labelHeight,
|
'height': labelHeight,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}), clickable.firstChild);
|
}), clickable.firstChild);
|
||||||
|
|
||||||
if(!first) {
|
if(!first) {
|
||||||
|
|
|
@ -37,11 +37,13 @@ define([
|
||||||
|
|
||||||
render(layer, theme, pt, dir) {
|
render(layer, theme, pt, dir) {
|
||||||
const config = this.getConfig(theme);
|
const config = this.getConfig(theme);
|
||||||
|
const short = this.short(theme);
|
||||||
layer.appendChild(config.render(config.attrs, {
|
layer.appendChild(config.render(config.attrs, {
|
||||||
x: pt.x + this.short(theme) * dir,
|
x: pt.x + short * dir.dx,
|
||||||
y: pt.y,
|
y: pt.y + short * dir.dy,
|
||||||
dx: config.width * dir,
|
width: config.width,
|
||||||
dy: config.height / 2,
|
height: config.height,
|
||||||
|
dir,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,8 +77,8 @@ define([
|
||||||
render(layer, theme, pt, dir) {
|
render(layer, theme, pt, dir) {
|
||||||
const config = this.getConfig(theme);
|
const config = this.getConfig(theme);
|
||||||
layer.appendChild(config.render({
|
layer.appendChild(config.render({
|
||||||
x: pt.x + config.short * dir,
|
x: pt.x + config.short * dir.dx,
|
||||||
y: pt.y,
|
y: pt.y + config.short * dir.dy,
|
||||||
radius: config.radius,
|
radius: config.radius,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -165,24 +167,51 @@ define([
|
||||||
array.mergeSets(env.momentaryAgentIDs, agentIDs);
|
array.mergeSets(env.momentaryAgentIDs, agentIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSelfConnect({label, agentIDs, options}, env) {
|
renderRevArrowLine({x1, y1, x2, y2, xR}, options, env) {
|
||||||
/* jshint -W071 */ // TODO: find appropriate abstractions
|
const config = env.theme.connect;
|
||||||
|
const line = config.line[options.line];
|
||||||
|
const lArrow = ARROWHEADS[options.left];
|
||||||
|
const rArrow = ARROWHEADS[options.right];
|
||||||
|
|
||||||
|
const dx1 = lArrow.lineGap(env.theme, line.attrs);
|
||||||
|
const dx2 = rArrow.lineGap(env.theme, line.attrs);
|
||||||
|
const rendered = line.renderRev(line.attrs, {
|
||||||
|
x1: x1 + dx1,
|
||||||
|
y1,
|
||||||
|
x2: x2 + dx2,
|
||||||
|
y2,
|
||||||
|
xR,
|
||||||
|
rad: config.loopbackRadius,
|
||||||
|
});
|
||||||
|
env.shapeLayer.appendChild(rendered.shape);
|
||||||
|
|
||||||
|
lArrow.render(env.shapeLayer, env.theme, {
|
||||||
|
x: rendered.p1.x - dx1,
|
||||||
|
y: rendered.p1.y,
|
||||||
|
}, {dx: 1, dy: 0});
|
||||||
|
|
||||||
|
rArrow.render(env.shapeLayer, env.theme, {
|
||||||
|
x: rendered.p2.x - dx2,
|
||||||
|
y: rendered.p2.y,
|
||||||
|
}, {dx: 1, dy: 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSelfConnect({label, agentIDs, options}, env, from, yBegin) {
|
||||||
const config = env.theme.connect;
|
const config = env.theme.connect;
|
||||||
const from = env.agentInfos.get(agentIDs[0]);
|
|
||||||
|
|
||||||
const lArrow = ARROWHEADS[options.left];
|
const lArrow = ARROWHEADS[options.left];
|
||||||
const rArrow = ARROWHEADS[options.right];
|
const rArrow = ARROWHEADS[options.right];
|
||||||
|
|
||||||
|
const to = env.agentInfos.get(agentIDs[1]);
|
||||||
|
|
||||||
const height = label ? (
|
const height = label ? (
|
||||||
env.textSizer.measureHeight(config.label.attrs, label) +
|
env.textSizer.measureHeight(config.label.attrs, label) +
|
||||||
config.label.margin.top +
|
config.label.margin.top +
|
||||||
config.label.margin.bottom
|
config.label.margin.bottom
|
||||||
) : 0;
|
) : 0;
|
||||||
|
|
||||||
const lineX = from.x + from.currentMaxRad;
|
const xL = (
|
||||||
const y0 = env.primaryY;
|
from.x + from.currentMaxRad +
|
||||||
const x0 = (
|
|
||||||
lineX +
|
|
||||||
lArrow.width(env.theme) +
|
lArrow.width(env.theme) +
|
||||||
(label ? config.label.padding : 0)
|
(label ? config.label.padding : 0)
|
||||||
);
|
);
|
||||||
|
@ -190,8 +219,8 @@ define([
|
||||||
const clickable = env.makeRegion();
|
const clickable = env.makeRegion();
|
||||||
|
|
||||||
const renderedText = SVGShapes.renderBoxedText(label, {
|
const renderedText = SVGShapes.renderBoxedText(label, {
|
||||||
x: x0 - config.mask.padding.left,
|
x: xL - config.mask.padding.left,
|
||||||
y: y0 - height + config.label.margin.top,
|
y: yBegin - height + config.label.margin.top,
|
||||||
padding: config.mask.padding,
|
padding: config.mask.padding,
|
||||||
boxAttrs: {'fill': '#000000'},
|
boxAttrs: {'fill': '#000000'},
|
||||||
labelAttrs: config.label.loopbackAttrs,
|
labelAttrs: config.label.loopbackAttrs,
|
||||||
|
@ -205,68 +234,74 @@ define([
|
||||||
config.mask.padding.left -
|
config.mask.padding.left -
|
||||||
config.mask.padding.right
|
config.mask.padding.right
|
||||||
) : 0);
|
) : 0);
|
||||||
const r = config.loopbackRadius;
|
|
||||||
const x1 = Math.max(lineX + rArrow.width(env.theme), x0 + labelW);
|
|
||||||
const y1 = y0 + r * 2;
|
|
||||||
|
|
||||||
const line = config.line[options.line];
|
const xR = Math.max(
|
||||||
const rendered = line.renderRev(line.attrs, {
|
to.x + to.currentMaxRad + rArrow.width(env.theme),
|
||||||
xL: lineX,
|
xL + labelW
|
||||||
dx1: lArrow.lineGap(env.theme, line.attrs),
|
);
|
||||||
dx2: rArrow.lineGap(env.theme, line.attrs),
|
const y2 = Math.max(
|
||||||
y1: y0,
|
yBegin + config.loopbackRadius * 2,
|
||||||
y2: y1,
|
env.primaryY
|
||||||
xR: x1,
|
);
|
||||||
});
|
|
||||||
env.shapeLayer.appendChild(rendered.shape);
|
|
||||||
|
|
||||||
lArrow.render(env.shapeLayer, env.theme, rendered.p1, 1);
|
this.renderRevArrowLine({
|
||||||
rArrow.render(env.shapeLayer, env.theme, rendered.p2, 1);
|
x1: from.x + from.currentMaxRad,
|
||||||
|
y1: yBegin,
|
||||||
|
x2: to.x + to.currentMaxRad,
|
||||||
|
y2,
|
||||||
|
xR,
|
||||||
|
}, options, env);
|
||||||
|
|
||||||
const raise = Math.max(height, lArrow.height(env.theme) / 2);
|
const raise = Math.max(height, lArrow.height(env.theme) / 2);
|
||||||
const arrowDip = rArrow.height(env.theme) / 2;
|
const arrowDip = rArrow.height(env.theme) / 2;
|
||||||
|
|
||||||
clickable.insertBefore(svg.make('rect', {
|
clickable.insertBefore(svg.make('rect', {
|
||||||
'x': lineX,
|
'x': from.x,
|
||||||
'y': y0 - raise,
|
'y': yBegin - raise,
|
||||||
'width': x1 + r - lineX,
|
'width': xR + config.loopbackRadius - from.x,
|
||||||
'height': raise + r * 2 + arrowDip,
|
'height': raise + y2 - yBegin + arrowDip,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}), clickable.firstChild);
|
}), clickable.firstChild);
|
||||||
|
|
||||||
return y1 + Math.max(
|
return y2 + Math.max(arrowDip, 0) + env.theme.actionMargin;
|
||||||
arrowDip + env.theme.minActionMargin,
|
|
||||||
env.theme.actionMargin
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSimpleLine(x0, x1, options, env) {
|
renderArrowLine({x1, y1, x2, y2}, options, env) {
|
||||||
const dir = (x0 < x1) ? 1 : -1;
|
|
||||||
|
|
||||||
const config = env.theme.connect;
|
const config = env.theme.connect;
|
||||||
const line = config.line[options.line];
|
const line = config.line[options.line];
|
||||||
const lArrow = ARROWHEADS[options.left];
|
const lArrow = ARROWHEADS[options.left];
|
||||||
const rArrow = ARROWHEADS[options.right];
|
const rArrow = ARROWHEADS[options.right];
|
||||||
|
|
||||||
|
const len = Math.sqrt(
|
||||||
|
(x2 - x1) * (x2 - x1) +
|
||||||
|
(y2 - y1) * (y2 - y1)
|
||||||
|
);
|
||||||
|
const d1 = lArrow.lineGap(env.theme, line.attrs);
|
||||||
|
const d2 = rArrow.lineGap(env.theme, line.attrs);
|
||||||
|
const dx = (x2 - x1) / len;
|
||||||
|
const dy = (y2 - y1) / len;
|
||||||
|
|
||||||
const rendered = line.renderFlat(line.attrs, {
|
const rendered = line.renderFlat(line.attrs, {
|
||||||
x1: x0,
|
x1: x1 + d1 * dx,
|
||||||
dx1: lArrow.lineGap(env.theme, line.attrs) * dir,
|
y1: y1 + d1 * dy,
|
||||||
x2: x1,
|
x2: x2 - d2 * dx,
|
||||||
dx2: -rArrow.lineGap(env.theme, line.attrs) * dir,
|
y2: y2 - d2 * dy,
|
||||||
y: env.primaryY,
|
|
||||||
});
|
});
|
||||||
env.shapeLayer.appendChild(rendered.shape);
|
env.shapeLayer.appendChild(rendered.shape);
|
||||||
return rendered;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSimpleArrowheads(options, renderedLine, env, dir) {
|
const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy};
|
||||||
const lArrow = ARROWHEADS[options.left];
|
const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy};
|
||||||
const rArrow = ARROWHEADS[options.right];
|
|
||||||
|
|
||||||
lArrow.render(env.shapeLayer, env.theme, renderedLine.p1, dir);
|
lArrow.render(env.shapeLayer, env.theme, p1, {dx, dy});
|
||||||
rArrow.render(env.shapeLayer, env.theme, renderedLine.p2, -dir);
|
rArrow.render(env.shapeLayer, env.theme, p2, {dx: -dx, dy: -dy});
|
||||||
|
|
||||||
return {lArrow, rArrow};
|
return {
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
lArrow,
|
||||||
|
rArrow,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderVirtualSources(from, to, renderedLine, env) {
|
renderVirtualSources(from, to, renderedLine, env) {
|
||||||
|
@ -288,9 +323,41 @@ define([
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSimpleConnect({label, agentIDs, options}, env) {
|
renderSimpleLabel(label, {layer, x1, x2, y1, y2, height}, env) {
|
||||||
|
const config = env.theme.connect;
|
||||||
|
|
||||||
|
const midX = (x1 + x2) / 2;
|
||||||
|
const midY = (y1 + y2) / 2;
|
||||||
|
|
||||||
|
let labelLayer = layer;
|
||||||
|
const boxAttrs = {'fill': '#000000'};
|
||||||
|
if(y1 !== y2) {
|
||||||
|
const angle = Math.atan((y2 - y1) / (x2 - x1));
|
||||||
|
const transform = (
|
||||||
|
'rotate(' +
|
||||||
|
(angle * 180 / Math.PI) +
|
||||||
|
' ' + midX + ',' + midY +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
boxAttrs.transform = transform;
|
||||||
|
labelLayer = svg.make('g', {'transform': transform});
|
||||||
|
layer.appendChild(labelLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
SVGShapes.renderBoxedText(label, {
|
||||||
|
x: midX,
|
||||||
|
y: midY + config.label.margin.top - height,
|
||||||
|
padding: config.mask.padding,
|
||||||
|
boxAttrs,
|
||||||
|
labelAttrs: config.label.attrs,
|
||||||
|
boxLayer: env.maskLayer,
|
||||||
|
labelLayer,
|
||||||
|
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSimpleConnect({label, agentIDs, options}, env, from, yBegin) {
|
||||||
const config = env.theme.connect;
|
const config = env.theme.connect;
|
||||||
const from = env.agentInfos.get(agentIDs[0]);
|
|
||||||
const to = env.agentInfos.get(agentIDs[1]);
|
const to = env.agentInfos.get(agentIDs[1]);
|
||||||
|
|
||||||
const dir = (from.x < to.x) ? 1 : -1;
|
const dir = (from.x < to.x) ? 1 : -1;
|
||||||
|
@ -301,44 +368,49 @@ define([
|
||||||
config.label.margin.bottom
|
config.label.margin.bottom
|
||||||
);
|
);
|
||||||
|
|
||||||
const x0 = from.x + from.currentMaxRad * dir;
|
const x1 = from.x + from.currentMaxRad * dir;
|
||||||
const x1 = to.x - to.currentMaxRad * dir;
|
const x2 = to.x - to.currentMaxRad * dir;
|
||||||
const y = env.primaryY;
|
|
||||||
|
|
||||||
const clickable = env.makeRegion();
|
const clickable = env.makeRegion();
|
||||||
|
|
||||||
SVGShapes.renderBoxedText(label, {
|
const rendered = this.renderArrowLine({
|
||||||
x: (x0 + x1) / 2,
|
x1,
|
||||||
y: y - height + config.label.margin.top,
|
y1: yBegin,
|
||||||
padding: config.mask.padding,
|
x2,
|
||||||
boxAttrs: {'fill': '#000000'},
|
y2: env.primaryY,
|
||||||
labelAttrs: config.label.attrs,
|
}, options, env);
|
||||||
boxLayer: env.maskLayer,
|
|
||||||
labelLayer: clickable,
|
|
||||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rendered = this.renderSimpleLine(x0, x1, options, env);
|
|
||||||
const {
|
|
||||||
lArrow,
|
|
||||||
rArrow
|
|
||||||
} = this.renderSimpleArrowheads(options, rendered, env, dir);
|
|
||||||
this.renderVirtualSources(from, to, rendered, env);
|
|
||||||
|
|
||||||
const arrowSpread = Math.max(
|
const arrowSpread = Math.max(
|
||||||
lArrow.height(env.theme),
|
rendered.lArrow.height(env.theme),
|
||||||
rArrow.height(env.theme)
|
rendered.rArrow.height(env.theme)
|
||||||
) / 2;
|
) / 2;
|
||||||
|
|
||||||
clickable.insertBefore(svg.make('rect', {
|
const lift = Math.max(height, arrowSpread);
|
||||||
'x': Math.min(x0, x1),
|
|
||||||
'y': y - Math.max(height, arrowSpread),
|
|
||||||
'width': Math.abs(x1 - x0),
|
|
||||||
'height': Math.max(height, arrowSpread) + arrowSpread,
|
|
||||||
'fill': 'transparent',
|
|
||||||
}), clickable.firstChild);
|
|
||||||
|
|
||||||
return y + Math.max(
|
this.renderVirtualSources(from, to, rendered, env);
|
||||||
|
|
||||||
|
clickable.appendChild(svg.make('path', {
|
||||||
|
'd': (
|
||||||
|
'M' + x1 + ',' + (yBegin - lift) +
|
||||||
|
'L' + x2 + ',' + (env.primaryY - lift) +
|
||||||
|
'L' + x2 + ',' + (env.primaryY + arrowSpread) +
|
||||||
|
'L' + x1 + ',' + (yBegin + arrowSpread) +
|
||||||
|
'Z'
|
||||||
|
),
|
||||||
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.renderSimpleLabel(label, {
|
||||||
|
layer: clickable,
|
||||||
|
x1,
|
||||||
|
y1: yBegin,
|
||||||
|
x2,
|
||||||
|
y2: env.primaryY,
|
||||||
|
height,
|
||||||
|
}, env);
|
||||||
|
|
||||||
|
return env.primaryY + Math.max(
|
||||||
arrowSpread + env.theme.minActionMargin,
|
arrowSpread + env.theme.minActionMargin,
|
||||||
env.theme.actionMargin
|
env.theme.actionMargin
|
||||||
);
|
);
|
||||||
|
@ -367,16 +439,77 @@ define([
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render(stage, env) {
|
render(stage, env, from = null, yBegin = null) {
|
||||||
if(stage.agentIDs[0] === stage.agentIDs[1]) {
|
if(from === null) {
|
||||||
return this.renderSelfConnect(stage, env);
|
from = env.agentInfos.get(stage.agentIDs[0]);
|
||||||
} else {
|
yBegin = env.primaryY;
|
||||||
return this.renderSimpleConnect(stage, env);
|
|
||||||
}
|
}
|
||||||
|
if(stage.agentIDs[0] === stage.agentIDs[1]) {
|
||||||
|
return this.renderSelfConnect(stage, env, from, yBegin);
|
||||||
|
} else {
|
||||||
|
return this.renderSimpleConnect(stage, env, from, yBegin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectDelayBegin extends Connect {
|
||||||
|
makeState(state) {
|
||||||
|
state.delayedConnections = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetState(state) {
|
||||||
|
state.delayedConnections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
separation(stage, env) {
|
||||||
|
super.separation(stage, env);
|
||||||
|
array.mergeSets(env.momentaryAgentIDs, [stage.agentIDs[0]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPre(stage, env) {
|
||||||
|
return Object.assign(super.renderPre(stage, env), {
|
||||||
|
agentIDs: [stage.agentIDs[0]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(stage, env) {
|
||||||
|
const dc = env.state.delayedConnections;
|
||||||
|
dc.set(stage.tag, {
|
||||||
|
stage,
|
||||||
|
from: Object.assign({}, env.agentInfos.get(stage.agentIDs[0])),
|
||||||
|
y: env.primaryY,
|
||||||
|
});
|
||||||
|
return env.primaryY + env.theme.actionMargin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectDelayEnd extends Connect {
|
||||||
|
separationPre() {}
|
||||||
|
|
||||||
|
separation() {}
|
||||||
|
|
||||||
|
renderPre({tag}, env) {
|
||||||
|
const dc = env.state.delayedConnections;
|
||||||
|
const beginStage = dc.get(tag).stage;
|
||||||
|
return Object.assign(super.renderPre(beginStage, env), {
|
||||||
|
agentIDs: [beginStage.agentIDs[1]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render({tag}, env) {
|
||||||
|
const dc = env.state.delayedConnections;
|
||||||
|
const begin = dc.get(tag);
|
||||||
|
return super.render(begin.stage, env, begin.from, begin.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseComponent.register('connect', new Connect());
|
BaseComponent.register('connect', new Connect());
|
||||||
|
BaseComponent.register('connect-delay-begin', new ConnectDelayBegin());
|
||||||
|
BaseComponent.register('connect-delay-end', new ConnectDelayEnd());
|
||||||
|
|
||||||
return Connect;
|
return {
|
||||||
|
Connect,
|
||||||
|
ConnectDelayBegin,
|
||||||
|
ConnectDelayEnd,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,11 @@ defineDescribe('Connect', [
|
||||||
|
|
||||||
it('registers itself with the component store', () => {
|
it('registers itself with the component store', () => {
|
||||||
const components = BaseComponent.getComponents();
|
const components = BaseComponent.getComponents();
|
||||||
expect(components.get('connect')).toEqual(jasmine.any(Connect));
|
expect(components.get('connect'))
|
||||||
|
.toEqual(jasmine.any(Connect.Connect));
|
||||||
|
expect(components.get('connect-delay-begin'))
|
||||||
|
.toEqual(jasmine.any(Connect.ConnectDelayBegin));
|
||||||
|
expect(components.get('connect-delay-end'))
|
||||||
|
.toEqual(jasmine.any(Connect.ConnectDelayEnd));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -82,6 +82,7 @@ define([
|
||||||
'width': right.x - left.x + config.extend * 2,
|
'width': right.x - left.x + config.extend * 2,
|
||||||
'height': fullHeight,
|
'height': fullHeight,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}), clickable.firstChild);
|
}), clickable.firstChild);
|
||||||
|
|
||||||
return env.primaryY + fullHeight + env.theme.actionMargin;
|
return env.primaryY + fullHeight + env.theme.actionMargin;
|
||||||
|
|
|
@ -92,6 +92,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
||||||
'width': x1 - x0,
|
'width': x1 - x0,
|
||||||
'height': fullH,
|
'height': fullH,
|
||||||
'fill': 'transparent',
|
'fill': 'transparent',
|
||||||
|
'class': 'vis',
|
||||||
}), clickable.firstChild);
|
}), clickable.firstChild);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -68,14 +68,18 @@ define([
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseTheme.renderHorizArrowHead = (attrs, {x, y, dx, dy}) => {
|
BaseTheme.renderArrowHead = (attrs, {x, y, width, height, dir}) => {
|
||||||
|
const wx = width * dir.dx;
|
||||||
|
const wy = width * dir.dy;
|
||||||
|
const hy = height * 0.5 * dir.dx;
|
||||||
|
const hx = -height * 0.5 * dir.dy;
|
||||||
return svg.make(
|
return svg.make(
|
||||||
attrs.fill === 'none' ? 'polyline' : 'polygon',
|
attrs.fill === 'none' ? 'polyline' : 'polygon',
|
||||||
Object.assign({
|
Object.assign({
|
||||||
'points': (
|
'points': (
|
||||||
(x + dx) + ' ' + (y - dy) + ' ' +
|
(x + wx - hx) + ' ' + (y + wy - hy) + ' ' +
|
||||||
x + ' ' + y + ' ' +
|
x + ' ' + y + ' ' +
|
||||||
(x + dx) + ' ' + (y + dy)
|
(x + wx + hx) + ' ' + (y + wy + hy)
|
||||||
),
|
),
|
||||||
}, attrs)
|
}, attrs)
|
||||||
);
|
);
|
||||||
|
@ -144,37 +148,50 @@ define([
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
BaseTheme.renderFlatConnector = (pattern, attrs, {x1, dx1, x2, dx2, y}) => {
|
BaseTheme.renderFlatConnector = (
|
||||||
|
pattern,
|
||||||
|
attrs,
|
||||||
|
{x1, y1, x2, y2}
|
||||||
|
) => {
|
||||||
return {
|
return {
|
||||||
shape: svg.make('path', Object.assign({
|
shape: svg.make('path', Object.assign({
|
||||||
d: new SVGShapes.PatternedLine(pattern)
|
d: new SVGShapes.PatternedLine(pattern)
|
||||||
.move(x1 + dx1, y)
|
.move(x1, y1)
|
||||||
.line(x2 + dx2, y)
|
.line(x2, y2)
|
||||||
.cap()
|
.cap()
|
||||||
.asPath(),
|
.asPath(),
|
||||||
}, attrs)),
|
}, attrs)),
|
||||||
p1: {x: x1, y},
|
p1: {x: x1, y: y1},
|
||||||
p2: {x: x2, y},
|
p2: {x: x2, y: y2},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
BaseTheme.renderRevConnector = (
|
BaseTheme.renderRevConnector = (
|
||||||
pattern,
|
pattern,
|
||||||
attrs,
|
attrs,
|
||||||
{xL, dx1, dx2, y1, y2, xR}
|
{x1, y1, x2, y2, xR, rad}
|
||||||
) => {
|
) => {
|
||||||
|
const maxRad = (y2 - y1) / 2;
|
||||||
|
const line = new SVGShapes.PatternedLine(pattern)
|
||||||
|
.move(x1, y1)
|
||||||
|
.line(xR, y1);
|
||||||
|
if(rad < maxRad) {
|
||||||
|
line
|
||||||
|
.arc(xR, y1 + rad, Math.PI / 2)
|
||||||
|
.line(xR + rad, y2 - rad)
|
||||||
|
.arc(xR, y2 - rad, Math.PI / 2);
|
||||||
|
} else {
|
||||||
|
line.arc(xR, (y1 + y2) / 2, Math.PI);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
shape: svg.make('path', Object.assign({
|
shape: svg.make('path', Object.assign({
|
||||||
d: new SVGShapes.PatternedLine(pattern)
|
d: line
|
||||||
.move(xL + dx1, y1)
|
.line(x2, y2)
|
||||||
.line(xR, y1)
|
|
||||||
.arc(xR, (y1 + y2) / 2, Math.PI)
|
|
||||||
.line(xL + dx2, y2)
|
|
||||||
.cap()
|
.cap()
|
||||||
.asPath(),
|
.asPath(),
|
||||||
}, attrs)),
|
}, attrs)),
|
||||||
p1: {x: xL, y: y1},
|
p1: {x: x1, y: y1},
|
||||||
p2: {x: xL, y: y2},
|
p2: {x: x2, y: y2},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ define([
|
||||||
'single': {
|
'single': {
|
||||||
width: 5,
|
width: 5,
|
||||||
height: 10,
|
height: 10,
|
||||||
render: BaseTheme.renderHorizArrowHead,
|
render: BaseTheme.renderArrowHead,
|
||||||
attrs: {
|
attrs: {
|
||||||
'fill': '#000000',
|
'fill': '#000000',
|
||||||
'stroke-width': 0,
|
'stroke-width': 0,
|
||||||
|
@ -117,7 +117,7 @@ define([
|
||||||
'double': {
|
'double': {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 6,
|
height: 6,
|
||||||
render: BaseTheme.renderHorizArrowHead,
|
render: BaseTheme.renderArrowHead,
|
||||||
attrs: {
|
attrs: {
|
||||||
'fill': 'none',
|
'fill': 'none',
|
||||||
'stroke': '#000000',
|
'stroke': '#000000',
|
||||||
|
|
|
@ -113,7 +113,7 @@ define([
|
||||||
'single': {
|
'single': {
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 12,
|
height: 12,
|
||||||
render: BaseTheme.renderHorizArrowHead,
|
render: BaseTheme.renderArrowHead,
|
||||||
attrs: {
|
attrs: {
|
||||||
'fill': '#000000',
|
'fill': '#000000',
|
||||||
'stroke': '#000000',
|
'stroke': '#000000',
|
||||||
|
@ -124,7 +124,7 @@ define([
|
||||||
'double': {
|
'double': {
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 12,
|
height: 12,
|
||||||
render: BaseTheme.renderHorizArrowHead,
|
render: BaseTheme.renderArrowHead,
|
||||||
attrs: {
|
attrs: {
|
||||||
'fill': 'none',
|
'fill': 'none',
|
||||||
'stroke': '#000000',
|
'stroke': '#000000',
|
||||||
|
|
|
@ -114,7 +114,7 @@ define([
|
||||||
'single': {
|
'single': {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 8,
|
height: 8,
|
||||||
render: BaseTheme.renderHorizArrowHead,
|
render: BaseTheme.renderArrowHead,
|
||||||
attrs: {
|
attrs: {
|
||||||
'fill': '#000000',
|
'fill': '#000000',
|
||||||
'stroke-width': 0,
|
'stroke-width': 0,
|
||||||
|
@ -124,7 +124,7 @@ define([
|
||||||
'double': {
|
'double': {
|
||||||
width: 3,
|
width: 3,
|
||||||
height: 6,
|
height: 6,
|
||||||
render: BaseTheme.renderHorizArrowHead,
|
render: BaseTheme.renderArrowHead,
|
||||||
attrs: {
|
attrs: {
|
||||||
'fill': 'none',
|
'fill': 'none',
|
||||||
'stroke': '#000000',
|
'stroke': '#000000',
|
||||||
|
|
|
@ -658,23 +658,23 @@ define([
|
||||||
return {shape};
|
return {shape};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFlatConnector(attrs, {x1, dx1, x2, dx2, y}) {
|
renderFlatConnector(attrs, {x1, y1, x2, y2}) {
|
||||||
const ln = this.lineNodes(
|
const ln = this.lineNodes(
|
||||||
{x: x1 + dx1, y},
|
{x: x1, y: y1},
|
||||||
{x: x2 + dx2, y},
|
{x: x2, y: y2},
|
||||||
{varX: 0.3}
|
{varX: 0.3}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)),
|
shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)),
|
||||||
p1: {x: ln.p1.x - dx1, y: ln.p1.y},
|
p1: ln.p1,
|
||||||
p2: {x: ln.p2.x - dx2, y: ln.p2.y},
|
p2: ln.p2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRevConnector(attrs, {xL, dx1, dx2, y1, y2, xR}) {
|
renderRevConnector(attrs, {x1, y1, x2, y2, xR}) {
|
||||||
const variance = Math.min((xR - xL) * 0.06, 3);
|
const variance = Math.min((xR - x1) * 0.06, 3);
|
||||||
const overshoot = Math.min((xR - xL) * 0.5, 6);
|
const overshoot = Math.min((xR - x1) * 0.5, 6);
|
||||||
const p1x = xL + dx1 + this.vary(variance, -1);
|
const p1x = x1 + this.vary(variance, -1);
|
||||||
const p1y = y1 + this.vary(variance, -1);
|
const p1y = y1 + this.vary(variance, -1);
|
||||||
const b1x = xR - overshoot * this.vary(0.2, 1);
|
const b1x = xR - overshoot * this.vary(0.2, 1);
|
||||||
const b1y = y1 - this.vary(1, 2);
|
const b1y = y1 - this.vary(1, 2);
|
||||||
|
@ -682,7 +682,7 @@ define([
|
||||||
const p2y = y1 + this.vary(1, 1);
|
const p2y = y1 + this.vary(1, 1);
|
||||||
const b2x = xR;
|
const b2x = xR;
|
||||||
const b2y = y2 + this.vary(2);
|
const b2y = y2 + this.vary(2);
|
||||||
const p3x = xL + dx2 + this.vary(variance, -1);
|
const p3x = x2 + this.vary(variance, -1);
|
||||||
const p3y = y2 + this.vary(variance, -1);
|
const p3y = y2 + this.vary(variance, -1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -696,41 +696,21 @@ define([
|
||||||
',' + p3x + ' ' + p3y
|
',' + p3x + ' ' + p3y
|
||||||
),
|
),
|
||||||
}, attrs)),
|
}, attrs)),
|
||||||
p1: {x: p1x - dx1, y: p1y},
|
p1: {x: p1x, y: p1y},
|
||||||
p2: {x: p3x - dx2, y: p3y},
|
p2: {x: p3x, y: p3y},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFlatConnectorWave(attrs, {x1, dx1, x2, dx2, y}) {
|
renderFlatConnectorWave(attrs, {x1, y1, x2, y2}) {
|
||||||
const x1v = x1 + this.vary(0.3);
|
const x1v = x1 + this.vary(0.3);
|
||||||
const x2v = x2 + this.vary(0.3);
|
const x2v = x2 + this.vary(0.3);
|
||||||
const y1v = y + this.vary(1);
|
|
||||||
const y2v = y + this.vary(1);
|
|
||||||
return {
|
|
||||||
shape: svg.make('path', Object.assign({
|
|
||||||
d: new SVGShapes.PatternedLine(this.wave)
|
|
||||||
.move(x1v + dx1, y1v)
|
|
||||||
.line(x2v + dx2, y2v)
|
|
||||||
.cap()
|
|
||||||
.asPath(),
|
|
||||||
}, attrs)),
|
|
||||||
p1: {x: x1v, y: y1v},
|
|
||||||
p2: {x: x2v, y: y2v},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRevConnectorWave(attrs, {xL, dx1, dx2, y1, y2, xR}) {
|
|
||||||
const x1v = xL + this.vary(0.3);
|
|
||||||
const x2v = xL + this.vary(0.3);
|
|
||||||
const y1v = y1 + this.vary(1);
|
const y1v = y1 + this.vary(1);
|
||||||
const y2v = y2 + this.vary(1);
|
const y2v = y2 + this.vary(1);
|
||||||
return {
|
return {
|
||||||
shape: svg.make('path', Object.assign({
|
shape: svg.make('path', Object.assign({
|
||||||
d: new SVGShapes.PatternedLine(this.wave)
|
d: new SVGShapes.PatternedLine(this.wave)
|
||||||
.move(x1v + dx1, y1v)
|
.move(x1v, y1v)
|
||||||
.line(xR, y1)
|
.line(x2v, y2v)
|
||||||
.arc(xR, (y1 + y2) / 2, Math.PI)
|
|
||||||
.line(x2v + dx2, y2v)
|
|
||||||
.cap()
|
.cap()
|
||||||
.asPath(),
|
.asPath(),
|
||||||
}, attrs)),
|
}, attrs)),
|
||||||
|
@ -739,17 +719,41 @@ define([
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderArrowHead(attrs, {x, y, dx, dy}) {
|
renderRevConnectorWave(attrs, {x1, y1, x2, y2, xR}) {
|
||||||
const w = dx * this.vary(0.2, 1);
|
const x1v = x1 + this.vary(0.3);
|
||||||
const h = dy * this.vary(0.3, 1);
|
const x2v = x2 + this.vary(0.3);
|
||||||
|
const y1v = y1 + this.vary(1);
|
||||||
|
const y2v = y2 + this.vary(1);
|
||||||
|
return {
|
||||||
|
shape: svg.make('path', Object.assign({
|
||||||
|
d: new SVGShapes.PatternedLine(this.wave)
|
||||||
|
.move(x1v, y1v)
|
||||||
|
.line(xR, y1)
|
||||||
|
.arc(xR, (y1 + y2) / 2, Math.PI)
|
||||||
|
.line(x2v, y2v)
|
||||||
|
.cap()
|
||||||
|
.asPath(),
|
||||||
|
}, attrs)),
|
||||||
|
p1: {x: x1v, y: y1v},
|
||||||
|
p2: {x: x2v, y: y2v},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderArrowHead(attrs, {x, y, width, height, dir}) {
|
||||||
|
const w = width * this.vary(0.2, 1);
|
||||||
|
const h = height * this.vary(0.3, 1);
|
||||||
|
const wx = w * dir.dx;
|
||||||
|
const wy = w * dir.dy;
|
||||||
|
const hy = h * 0.5 * dir.dx;
|
||||||
|
const hx = -h * 0.5 * dir.dy;
|
||||||
const l1 = this.lineNodes(
|
const l1 = this.lineNodes(
|
||||||
{x: x + w, y: y - h},
|
{x: x + wx - hx, y: y + wy - hy},
|
||||||
{x, y},
|
{x, y},
|
||||||
{var1: 2.0, var2: 0.2}
|
{var1: 2.0, var2: 0.2}
|
||||||
);
|
);
|
||||||
const l2 = this.lineNodes(
|
const l2 = this.lineNodes(
|
||||||
l1.p2,
|
l1.p2,
|
||||||
{x: x + w, y: y + h},
|
{x: x + wx + hx, y: y + wy + hy},
|
||||||
{var1: 0, var2: 2.0, move: false}
|
{var1: 0, var2: 2.0, move: false}
|
||||||
);
|
);
|
||||||
const l3 = (attrs.fill === 'none') ? {nodes: ''} : this.lineNodes(
|
const l3 = (attrs.fill === 'none') ? {nodes: ''} : this.lineNodes(
|
||||||
|
|
|
@ -73,7 +73,7 @@ define(() => {
|
||||||
(cx - this.x) * (cx - this.x) +
|
(cx - this.x) * (cx - this.x) +
|
||||||
(cy - this.y) * (cy - this.y)
|
(cy - this.y) * (cy - this.y)
|
||||||
);
|
);
|
||||||
const theta1 = Math.atan2(cx - this.x, cy - this.y);
|
const theta1 = Math.atan2(this.x - cx, cy - this.y);
|
||||||
const nextX = cx + Math.sin(theta1 + theta) * radius;
|
const nextX = cx + Math.sin(theta1 + theta) * radius;
|
||||||
const nextY = cy - Math.cos(theta1 + theta) * radius;
|
const nextY = cy - Math.cos(theta1 + theta) * radius;
|
||||||
|
|
||||||
|
@ -93,8 +93,8 @@ define(() => {
|
||||||
this.points.push(
|
this.points.push(
|
||||||
this.x + ' ' + this.y +
|
this.x + ' ' + this.y +
|
||||||
'A' + radius + ' ' + radius + ' 0 ' +
|
'A' + radius + ' ' + radius + ' 0 ' +
|
||||||
|
((Math.abs(theta) >= Math.PI) ? '1 ' : '0 ') +
|
||||||
((theta < 0) ? '0 ' : '1 ') +
|
((theta < 0) ? '0 ' : '1 ') +
|
||||||
'1 ' +
|
|
||||||
nextX + ' ' + nextY
|
nextX + ' ' + nextY
|
||||||
);
|
);
|
||||||
this.disconnect = 0;
|
this.disconnect = 0;
|
||||||
|
|
|
@ -30,6 +30,26 @@ defineDescribe('PatternedLine', ['./PatternedLine'], (PatternedLine) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports quarter arcs', () => {
|
||||||
|
const ln = new PatternedLine()
|
||||||
|
.move(10, 20)
|
||||||
|
.arc(10, 30, Math.PI / 2)
|
||||||
|
.arc(10, 30, Math.PI / 2)
|
||||||
|
.arc(10, 30, Math.PI / 2)
|
||||||
|
.arc(10, 30, Math.PI / 2);
|
||||||
|
|
||||||
|
expect(simplify(ln.asPath(), 0)).toEqual(
|
||||||
|
'M10 20' +
|
||||||
|
'A10 10 0 0 1 20 30' +
|
||||||
|
'L20 30' +
|
||||||
|
'A10 10 0 0 1 10 40' +
|
||||||
|
'L10 40' +
|
||||||
|
'A10 10 0 0 1 0 30' +
|
||||||
|
'L0 30' +
|
||||||
|
'A10 10 0 0 1 10 20'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('can combine lines and arcs', () => {
|
it('can combine lines and arcs', () => {
|
||||||
const ln = new PatternedLine()
|
const ln = new PatternedLine()
|
||||||
.move(10, 20)
|
.move(10, 20)
|
||||||
|
|
|
@ -153,17 +153,17 @@ html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-view .region:hover rect {
|
.pane-view .region:hover .vis {
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
stroke: rgba(255, 255, 0, 0.5);
|
stroke: rgba(255, 255, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-view .region.focus rect {
|
.pane-view .region.focus .vis {
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
stroke: rgba(255, 128, 0, 0.5);
|
stroke: rgba(255, 128, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-view .region.focus:hover rect {
|
.pane-view .region.focus:hover .vis {
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
stroke: rgba(255, 192, 0, 0.5);
|
stroke: rgba(255, 192, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue