c(e))}})}),e(["sequence/SequenceDiagram"],e=>{"use strict";const t=window.define;t&&t.amd?t(()=>e):(document.addEventListener("DOMContentLoaded",()=>{e.convertAll()},{once:!0}),window.CodeMirror&&e.registerCodeMirrorMode(window.CodeMirror),window.SequenceDiagram=e)},null,!0),n("standalone",function(){})}();
\ No newline at end of file
diff --git a/library.htm b/library.htm
index d8e9ea7..11c2cb8 100644
--- a/library.htm
+++ b/library.htm
@@ -343,6 +343,25 @@ Foo -> Bar
Bar -> Baz
+Asynchronous Communication
+
+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
+
+
More
diff --git a/screenshots/AsynchronousCommunication.png b/screenshots/AsynchronousCommunication.png
new file mode 100644
index 0000000..1d76eee
Binary files /dev/null and b/screenshots/AsynchronousCommunication.png differ
diff --git a/scripts/editor.js b/scripts/editor.js
index 238244a..95cfac0 100644
--- a/scripts/editor.js
+++ b/scripts/editor.js
@@ -56,6 +56,15 @@
title: 'Self-connection',
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',
code: '* -> {Agent1}: {Message}',
diff --git a/scripts/sequence/CodeMirrorHints.js b/scripts/sequence/CodeMirrorHints.js
index 04f4ebe..94546f3 100644
--- a/scripts/sequence/CodeMirrorHints.js
+++ b/scripts/sequence/CodeMirrorHints.js
@@ -2,8 +2,11 @@ define(['core/ArrayUtilities'], (array) => {
'use strict';
const TRIMMER = /^([ \t]*)(.*)$/;
- const SQUASH_START = /^[ \t\r\n:,]/;
- const SQUASH_END = /[ \t\r\n]$/;
+ const SQUASH = {
+ start: /^[ \t\r\n:,]/,
+ end: /[ \t\r\n]$/,
+ after: '.!+', // cannot squash after * or - in all cases
+ };
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
const QUOTE_ESCAPE = /["\\]/g;
@@ -24,6 +27,9 @@ define(['core/ArrayUtilities'], (array) => {
squash: {line: line, ch: chFrom},
};
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
+ if(SQUASH.after.includes(ln[chFrom - 2])) {
+ ranges.word.ch --;
+ }
ranges.squash.ch --;
}
return ranges;
@@ -69,8 +75,8 @@ define(['core/ArrayUtilities'], (array) => {
text: quoted,
displayText: quoted.trim(),
className: null,
- from: SQUASH_START.test(quoted) ? from.squash : from.word,
- to: SQUASH_END.test(quoted) ? ranges.to.squash : ranges.to.word,
+ from: SQUASH.start.test(quoted) ? from.squash : from.word,
+ to: SQUASH.end.test(quoted) ? ranges.to.squash : ranges.to.word,
displayFrom: from.word,
};
}
diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js
index 810924b..38870ac 100644
--- a/scripts/sequence/CodeMirrorMode.js
+++ b/scripts/sequence/CodeMirrorMode.js
@@ -205,24 +205,67 @@ define(['core/ArrayUtilities'], (array) => {
const connect = {
type: 'keyword',
suggest: true,
- then: makeOpBlock(agentToOptText, {
+ then: Object.assign({}, makeOpBlock(agentToOptText, {
':': colonTextToEnd,
'\n': hiddenEnd,
+ }), {
+ '...': {type: 'operator', suggest: true, then: {
+ '': {
+ type: 'variable',
+ suggest: {known: 'DelayedAgent'},
+ then: {
+ '': 0,
+ ':': CM_ERROR,
+ '\n': end,
+ },
+ },
+ }},
}),
};
- const then = {'': 0};
- arrows.forEach((arrow) => (then[arrow] = connect));
- then[':'] = {
+ const connectors = {};
+ arrows.forEach((arrow) => (connectors[arrow] = connect));
+
+ const labelIndicator = {
type: 'operator',
suggest: true,
override: 'Label',
then: {},
};
- return makeOpBlock(
- {type: 'variable', suggest: {known: 'Agent'}, then},
- then
- );
+
+ const hiddenLabelIndicator = {
+ 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 = {
@@ -480,6 +523,7 @@ define(['core/ArrayUtilities'], (array) => {
currentSpace: '',
currentQuoted: false,
knownAgent: [],
+ knownDelayedAgent: [],
knownLabel: [],
beginCompletions: cmMakeCompletions({}, [this.commands]),
completions: [],
diff --git a/scripts/sequence/CodeMirrorMode_spec.js b/scripts/sequence/CodeMirrorMode_spec.js
index 7f1b246..85127f9 100644
--- a/scripts/sequence/CodeMirrorMode_spec.js
+++ b/scripts/sequence/CodeMirrorMode_spec.js
@@ -139,7 +139,7 @@ defineDescribe('Code Mirror Mode', [
]);
});
- it('recognises agent flags', () => {
+ it('recognises agent flags on the right', () => {
cm.getDoc().setValue('Foo -> *Bar');
expect(getTokens(0)).toEqual([
{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', () => {
cm.getDoc().setValue('+ -> Bar');
expect(getTokens(0)[2].type).toContain('line-error');
@@ -198,6 +208,30 @@ defineDescribe('Code Mirror Mode', [
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', () => {
cm.getDoc().setValue(
'if\n' +
@@ -491,6 +525,7 @@ defineDescribe('Code Mirror Mode', [
'* ',
'! ',
'Foo ',
+ '... ',
]);
});
@@ -572,5 +607,10 @@ defineDescribe('Code Mirror Mode', [
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 ']);
+ });
});
});
diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js
index 03a7cc3..3ac434e 100644
--- a/scripts/sequence/Generator.js
+++ b/scripts/sequence/Generator.js
@@ -256,6 +256,8 @@ define(['core/ArrayUtilities'], (array) => {
'divider': this.handleDivider.bind(this),
'label pattern': this.handleLabelPattern.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 left': this.handleNote.bind(this),
'note right': this.handleNote.bind(this),
@@ -436,23 +438,39 @@ 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}) {
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
const rightGAgent = GAgent.make(name + ']');
const gAgents = [leftGAgent, rightGAgent];
const stages = [];
- this.currentSection = {
- header: {
- type: 'block begin',
- blockType,
- tag: this.textFormatter(tag),
- label: this.textFormatter(label),
- left: leftGAgent.id,
- right: rightGAgent.id,
- ln,
- },
- stages,
- };
+ this.currentSection = this._makeSection({
+ type: 'block begin',
+ blockType,
+ tag: this.textFormatter(tag),
+ label: this.textFormatter(label),
+ left: leftGAgent.id,
+ right: rightGAgent.id,
+ ln,
+ }, stages);
this.currentNest = {
blockType,
gAgents,
@@ -496,18 +514,16 @@ define(['core/ArrayUtilities'], (array) => {
this.currentNest.blockType + ')'
);
}
- this.currentSection = {
- header: {
- type: 'block split',
- blockType,
- tag: this.textFormatter(tag),
- label: this.textFormatter(label),
- left: this.currentNest.leftGAgent.id,
- right: this.currentNest.rightGAgent.id,
- ln,
- },
- stages: [],
- };
+ this._checkSectionEnd();
+ this.currentSection = this._makeSection({
+ type: 'block split',
+ blockType,
+ tag: this.textFormatter(tag),
+ label: this.textFormatter(label),
+ left: this.currentNest.leftGAgent.id,
+ right: this.currentNest.rightGAgent.id,
+ ln,
+ }, []);
this.currentNest.sections.push(this.currentSection);
}
@@ -515,6 +531,7 @@ define(['core/ArrayUtilities'], (array) => {
if(this.nesting.length <= 1) {
throw new Error('Invalid block nesting (too many "end"s)');
}
+ this._checkSectionEnd();
const nested = this.nesting.pop();
this.currentNest = array.last(this.nesting);
this.currentSection = array.last(this.currentNest.sections);
@@ -793,24 +810,20 @@ define(['core/ArrayUtilities'], (array) => {
return gAgents;
}
- handleConnect({agents, label, options}) {
+ _handlePartialConnect(agents) {
const flags = this.filterConnectFlags(agents);
- let gAgents = agents.map(this.toGAgent);
+ const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents, {
allowGrouped: true,
allowVirtual: true,
});
- const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
- this.defineGAgents(allGAgents
+ this.defineGAgents(array
+ .flatMap(gAgents, this.expandGroupedGAgent)
.filter((gAgent) => !gAgent.isVirtualSource)
);
- gAgents = this.expandGroupedGAgentConnection(gAgents);
- gAgents = this.expandVirtualSourceAgents(gAgents);
- const agentIDs = gAgents.map((gAgent) => gAgent.id);
-
const implicitBeginGAgents = (agents
.filter(PAgent.hasFlag('begin', false))
.map(this.toGAgent)
@@ -818,20 +831,105 @@ define(['core/ArrayUtilities'], (array) => {
);
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
- const connectStage = {
- type: 'connect',
- agentIDs,
- label: this.textFormatter(this.applyLabelPattern(label)),
- options,
- };
+ return {flags, gAgents};
+ }
- this.addParallelStages([
+ _makeConnectParallelStages(flags, connectStage) {
+ return [
this.setGAgentVis(flags.beginGAgents, true, 'box', true),
this.setGAgentHighlight(flags.startGAgents, true, true),
connectStage,
this.setGAgentHighlight(flags.stopGAgents, false, 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}) {
@@ -952,6 +1050,8 @@ define(['core/ArrayUtilities'], (array) => {
throw new Error('Unterminated group');
}
+ this._checkSectionEnd();
+
const terminators = meta.terminators || 'none';
this.addParallelStages([
this.setGAgentHighlight(this.gAgents, false),
diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js
index f5f02c8..10b73af 100644
--- a/scripts/sequence/Generator_spec.js
+++ b/scripts/sequence/Generator_spec.js
@@ -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, {
mode = '',
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, {
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', () => {
const sequence = invoke([
PARSED.connect(['A', 'B']),
diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js
index 0d21987..57f2798 100644
--- a/scripts/sequence/Parser.js
+++ b/scripts/sequence/Parser.js
@@ -132,17 +132,6 @@ define([
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) {
if(end === null) {
end = line.length;
@@ -206,6 +195,19 @@ define([
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}) {
let aliasSep = -1;
if(enableAlias) {
@@ -215,7 +217,11 @@ define([
aliasSep = end;
}
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 {
name: joinLabel(line, start, aliasSep),
@@ -491,35 +497,54 @@ define([
},
(line) => { // connect
- let labelSep = findToken(line, ':');
- if(labelSep === -1) {
- labelSep = line.length;
- }
- let typePos = -1;
- let options = null;
- for(let j = 0; j < line.length; ++ j) {
- const opts = CONNECT.types.get(tokenKeyword(line[j]));
- if(opts) {
- typePos = j;
- options = opts;
- break;
- }
- }
- if(typePos <= 0 || typePos >= labelSep - 1) {
+ const labelSep = findToken(line, ':', {orEnd: true});
+ const connectionToken = findFirstToken(
+ line,
+ CONNECT.types,
+ {start: 0, limit: labelSep - 1}
+ );
+ if(!connectionToken) {
return null;
}
- const readAgentOpts = {
+
+ const connectPos = connectionToken.pos;
+
+ const readOpts = {
flagTypes: CONNECT.agentFlags,
};
- return {
- type: 'connect',
- agents: [
- readAgent(line, 0, typePos, readAgentOpts),
- readAgent(line, typePos + 1, labelSep, readAgentOpts),
- ],
- label: joinLabel(line, labelSep + 1),
- options,
- };
+
+ 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 {
+ type: 'connect',
+ agents: [
+ readAgent(line, 0, connectPos, readOpts),
+ readAgent(line, connectPos + 1, labelSep, readOpts),
+ ],
+ label: joinLabel(line, labelSep + 1),
+ options: connectionToken.value,
+ };
+ }
},
(line) => { // marker
diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js
index 06e048e..7617590 100644
--- a/scripts/sequence/Parser_spec.js
+++ b/scripts/sequence/Parser_spec.js
@@ -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', () => {
const parsed = parser.parse('note over A: hello there');
expect(parsed.stages).toEqual([{
@@ -722,6 +759,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
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', () => {
expect(() => parser.parse('terminators foo')).toThrow(new Error(
'Unknown termination "foo" at line 1, character 12'
diff --git a/scripts/sequence/SequenceDiagram_spec.js b/scripts/sequence/SequenceDiagram_spec.js
index bfc13d4..20615ea 100644
--- a/scripts/sequence/SequenceDiagram_spec.js
+++ b/scripts/sequence/SequenceDiagram_spec.js
@@ -121,7 +121,7 @@ defineDescribe('SequenceDiagram', [
' {
escapeWith: unescape,
baseToken: {q: true},
},
+ {start: /\.\.\./y, baseToken: {v: '...'}},
{start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
{
start: /(?=[\-~<])/y,
diff --git a/scripts/sequence/Tokeniser_spec.js b/scripts/sequence/Tokeniser_spec.js
index 1956604..9ef648b 100644
--- a/scripts/sequence/Tokeniser_spec.js
+++ b/scripts/sequence/Tokeniser_spec.js
@@ -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', () => {
const input = 'foo bar\nbaz';
const tokens = tokeniser.tokenise(input);
diff --git a/scripts/sequence/components/AgentCap.js b/scripts/sequence/components/AgentCap.js
index aaf8b28..f7ae6e8 100644
--- a/scripts/sequence/components/AgentCap.js
+++ b/scripts/sequence/components/AgentCap.js
@@ -57,6 +57,7 @@ define([
'width': width,
'height': height,
'fill': 'transparent',
+ 'class': 'vis',
}), clickable.firstChild);
return {
@@ -94,6 +95,7 @@ define([
'width': d * 2,
'height': d * 2,
'fill': 'transparent',
+ 'class': 'vis',
}));
return {
@@ -148,6 +150,7 @@ define([
'width': width,
'height': height,
'fill': 'transparent',
+ 'class': 'vis',
}));
return {
@@ -208,6 +211,7 @@ define([
'width': config.width,
'height': config.height,
'fill': 'transparent',
+ 'class': 'vis',
}));
return {
@@ -242,6 +246,7 @@ define([
'width': w,
'height': config.height,
'fill': 'transparent',
+ 'class': 'vis',
}));
return {
diff --git a/scripts/sequence/components/Block.js b/scripts/sequence/components/Block.js
index 7a6c70b..0066f96 100644
--- a/scripts/sequence/components/Block.js
+++ b/scripts/sequence/components/Block.js
@@ -77,6 +77,7 @@ define([
'width': agentInfoR.x - agentInfoL.x,
'height': labelHeight,
'fill': 'transparent',
+ 'class': 'vis',
}), clickable.firstChild);
if(!first) {
diff --git a/scripts/sequence/components/Connect.js b/scripts/sequence/components/Connect.js
index 688e5c7..0fd08aa 100644
--- a/scripts/sequence/components/Connect.js
+++ b/scripts/sequence/components/Connect.js
@@ -37,11 +37,13 @@ define([
render(layer, theme, pt, dir) {
const config = this.getConfig(theme);
+ const short = this.short(theme);
layer.appendChild(config.render(config.attrs, {
- x: pt.x + this.short(theme) * dir,
- y: pt.y,
- dx: config.width * dir,
- dy: config.height / 2,
+ x: pt.x + short * dir.dx,
+ y: pt.y + short * dir.dy,
+ width: config.width,
+ height: config.height,
+ dir,
}));
}
@@ -75,8 +77,8 @@ define([
render(layer, theme, pt, dir) {
const config = this.getConfig(theme);
layer.appendChild(config.render({
- x: pt.x + config.short * dir,
- y: pt.y,
+ x: pt.x + config.short * dir.dx,
+ y: pt.y + config.short * dir.dy,
radius: config.radius,
}));
}
@@ -165,24 +167,51 @@ define([
array.mergeSets(env.momentaryAgentIDs, agentIDs);
}
- renderSelfConnect({label, agentIDs, options}, env) {
- /* jshint -W071 */ // TODO: find appropriate abstractions
+ renderRevArrowLine({x1, y1, x2, y2, xR}, options, env) {
+ 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 from = env.agentInfos.get(agentIDs[0]);
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
+ const to = env.agentInfos.get(agentIDs[1]);
+
const height = label ? (
env.textSizer.measureHeight(config.label.attrs, label) +
config.label.margin.top +
config.label.margin.bottom
) : 0;
- const lineX = from.x + from.currentMaxRad;
- const y0 = env.primaryY;
- const x0 = (
- lineX +
+ const xL = (
+ from.x + from.currentMaxRad +
lArrow.width(env.theme) +
(label ? config.label.padding : 0)
);
@@ -190,8 +219,8 @@ define([
const clickable = env.makeRegion();
const renderedText = SVGShapes.renderBoxedText(label, {
- x: x0 - config.mask.padding.left,
- y: y0 - height + config.label.margin.top,
+ x: xL - config.mask.padding.left,
+ y: yBegin - height + config.label.margin.top,
padding: config.mask.padding,
boxAttrs: {'fill': '#000000'},
labelAttrs: config.label.loopbackAttrs,
@@ -205,68 +234,74 @@ define([
config.mask.padding.left -
config.mask.padding.right
) : 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 rendered = line.renderRev(line.attrs, {
- xL: lineX,
- dx1: lArrow.lineGap(env.theme, line.attrs),
- dx2: rArrow.lineGap(env.theme, line.attrs),
- y1: y0,
- y2: y1,
- xR: x1,
- });
- env.shapeLayer.appendChild(rendered.shape);
+ const xR = Math.max(
+ to.x + to.currentMaxRad + rArrow.width(env.theme),
+ xL + labelW
+ );
+ const y2 = Math.max(
+ yBegin + config.loopbackRadius * 2,
+ env.primaryY
+ );
- lArrow.render(env.shapeLayer, env.theme, rendered.p1, 1);
- rArrow.render(env.shapeLayer, env.theme, rendered.p2, 1);
+ this.renderRevArrowLine({
+ 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 arrowDip = rArrow.height(env.theme) / 2;
clickable.insertBefore(svg.make('rect', {
- 'x': lineX,
- 'y': y0 - raise,
- 'width': x1 + r - lineX,
- 'height': raise + r * 2 + arrowDip,
+ 'x': from.x,
+ 'y': yBegin - raise,
+ 'width': xR + config.loopbackRadius - from.x,
+ 'height': raise + y2 - yBegin + arrowDip,
'fill': 'transparent',
+ 'class': 'vis',
}), clickable.firstChild);
- return y1 + Math.max(
- arrowDip + env.theme.minActionMargin,
- env.theme.actionMargin
- );
+ return y2 + Math.max(arrowDip, 0) + env.theme.actionMargin;
}
- renderSimpleLine(x0, x1, options, env) {
- const dir = (x0 < x1) ? 1 : -1;
-
+ renderArrowLine({x1, y1, x2, y2}, options, env) {
const config = env.theme.connect;
const line = config.line[options.line];
const lArrow = ARROWHEADS[options.left];
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, {
- x1: x0,
- dx1: lArrow.lineGap(env.theme, line.attrs) * dir,
- x2: x1,
- dx2: -rArrow.lineGap(env.theme, line.attrs) * dir,
- y: env.primaryY,
+ x1: x1 + d1 * dx,
+ y1: y1 + d1 * dy,
+ x2: x2 - d2 * dx,
+ y2: y2 - d2 * dy,
});
env.shapeLayer.appendChild(rendered.shape);
- return rendered;
- }
- renderSimpleArrowheads(options, renderedLine, env, dir) {
- const lArrow = ARROWHEADS[options.left];
- const rArrow = ARROWHEADS[options.right];
+ const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy};
+ const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy};
- lArrow.render(env.shapeLayer, env.theme, renderedLine.p1, dir);
- rArrow.render(env.shapeLayer, env.theme, renderedLine.p2, -dir);
+ lArrow.render(env.shapeLayer, env.theme, p1, {dx, dy});
+ rArrow.render(env.shapeLayer, env.theme, p2, {dx: -dx, dy: -dy});
- return {lArrow, rArrow};
+ return {
+ p1,
+ p2,
+ lArrow,
+ rArrow,
+ };
}
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 from = env.agentInfos.get(agentIDs[0]);
const to = env.agentInfos.get(agentIDs[1]);
const dir = (from.x < to.x) ? 1 : -1;
@@ -301,44 +368,49 @@ define([
config.label.margin.bottom
);
- const x0 = from.x + from.currentMaxRad * dir;
- const x1 = to.x - to.currentMaxRad * dir;
- const y = env.primaryY;
+ const x1 = from.x + from.currentMaxRad * dir;
+ const x2 = to.x - to.currentMaxRad * dir;
const clickable = env.makeRegion();
- SVGShapes.renderBoxedText(label, {
- x: (x0 + x1) / 2,
- y: y - height + config.label.margin.top,
- padding: config.mask.padding,
- boxAttrs: {'fill': '#000000'},
- labelAttrs: config.label.attrs,
- 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 rendered = this.renderArrowLine({
+ x1,
+ y1: yBegin,
+ x2,
+ y2: env.primaryY,
+ }, options, env);
const arrowSpread = Math.max(
- lArrow.height(env.theme),
- rArrow.height(env.theme)
+ rendered.lArrow.height(env.theme),
+ rendered.rArrow.height(env.theme)
) / 2;
- clickable.insertBefore(svg.make('rect', {
- '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);
+ const lift = Math.max(height, arrowSpread);
- 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,
env.theme.actionMargin
);
@@ -367,16 +439,77 @@ define([
};
}
- render(stage, env) {
+ render(stage, env, from = null, yBegin = null) {
+ if(from === null) {
+ from = env.agentInfos.get(stage.agentIDs[0]);
+ yBegin = env.primaryY;
+ }
if(stage.agentIDs[0] === stage.agentIDs[1]) {
- return this.renderSelfConnect(stage, env);
+ return this.renderSelfConnect(stage, env, from, yBegin);
} else {
- return this.renderSimpleConnect(stage, env);
+ return this.renderSimpleConnect(stage, env, from, yBegin);
}
}
}
- BaseComponent.register('connect', new Connect());
+ class ConnectDelayBegin extends Connect {
+ makeState(state) {
+ state.delayedConnections = new Map();
+ }
- return Connect;
+ 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-delay-begin', new ConnectDelayBegin());
+ BaseComponent.register('connect-delay-end', new ConnectDelayEnd());
+
+ return {
+ Connect,
+ ConnectDelayBegin,
+ ConnectDelayEnd,
+ };
});
diff --git a/scripts/sequence/components/Connect_spec.js b/scripts/sequence/components/Connect_spec.js
index 19e0ab0..5d23f92 100644
--- a/scripts/sequence/components/Connect_spec.js
+++ b/scripts/sequence/components/Connect_spec.js
@@ -9,6 +9,11 @@ defineDescribe('Connect', [
it('registers itself with the component store', () => {
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));
});
});
diff --git a/scripts/sequence/components/Divider.js b/scripts/sequence/components/Divider.js
index d27a286..0248d82 100644
--- a/scripts/sequence/components/Divider.js
+++ b/scripts/sequence/components/Divider.js
@@ -82,6 +82,7 @@ define([
'width': right.x - left.x + config.extend * 2,
'height': fullHeight,
'fill': 'transparent',
+ 'class': 'vis',
}), clickable.firstChild);
return env.primaryY + fullHeight + env.theme.actionMargin;
diff --git a/scripts/sequence/components/Note.js b/scripts/sequence/components/Note.js
index 2ac06fb..3dd3670 100644
--- a/scripts/sequence/components/Note.js
+++ b/scripts/sequence/components/Note.js
@@ -92,6 +92,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
'width': x1 - x0,
'height': fullH,
'fill': 'transparent',
+ 'class': 'vis',
}), clickable.firstChild);
return (
diff --git a/scripts/sequence/themes/BaseTheme.js b/scripts/sequence/themes/BaseTheme.js
index 21160c1..9ca5dc7 100644
--- a/scripts/sequence/themes/BaseTheme.js
+++ b/scripts/sequence/themes/BaseTheme.js
@@ -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(
attrs.fill === 'none' ? 'polyline' : 'polygon',
Object.assign({
'points': (
- (x + dx) + ' ' + (y - dy) + ' ' +
+ (x + wx - hx) + ' ' + (y + wy - hy) + ' ' +
x + ' ' + y + ' ' +
- (x + dx) + ' ' + (y + dy)
+ (x + wx + hx) + ' ' + (y + wy + hy)
),
}, attrs)
);
@@ -144,37 +148,50 @@ define([
}
};
- BaseTheme.renderFlatConnector = (pattern, attrs, {x1, dx1, x2, dx2, y}) => {
+ BaseTheme.renderFlatConnector = (
+ pattern,
+ attrs,
+ {x1, y1, x2, y2}
+ ) => {
return {
shape: svg.make('path', Object.assign({
d: new SVGShapes.PatternedLine(pattern)
- .move(x1 + dx1, y)
- .line(x2 + dx2, y)
+ .move(x1, y1)
+ .line(x2, y2)
.cap()
.asPath(),
}, attrs)),
- p1: {x: x1, y},
- p2: {x: x2, y},
+ p1: {x: x1, y: y1},
+ p2: {x: x2, y: y2},
};
};
BaseTheme.renderRevConnector = (
pattern,
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 {
shape: svg.make('path', Object.assign({
- d: new SVGShapes.PatternedLine(pattern)
- .move(xL + dx1, y1)
- .line(xR, y1)
- .arc(xR, (y1 + y2) / 2, Math.PI)
- .line(xL + dx2, y2)
+ d: line
+ .line(x2, y2)
.cap()
.asPath(),
}, attrs)),
- p1: {x: xL, y: y1},
- p2: {x: xL, y: y2},
+ p1: {x: x1, y: y1},
+ p2: {x: x2, y: y2},
};
};
diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js
index 3917d4f..1145889 100644
--- a/scripts/sequence/themes/Basic.js
+++ b/scripts/sequence/themes/Basic.js
@@ -107,7 +107,7 @@ define([
'single': {
width: 5,
height: 10,
- render: BaseTheme.renderHorizArrowHead,
+ render: BaseTheme.renderArrowHead,
attrs: {
'fill': '#000000',
'stroke-width': 0,
@@ -117,7 +117,7 @@ define([
'double': {
width: 4,
height: 6,
- render: BaseTheme.renderHorizArrowHead,
+ render: BaseTheme.renderArrowHead,
attrs: {
'fill': 'none',
'stroke': '#000000',
diff --git a/scripts/sequence/themes/Chunky.js b/scripts/sequence/themes/Chunky.js
index c3de690..752943b 100644
--- a/scripts/sequence/themes/Chunky.js
+++ b/scripts/sequence/themes/Chunky.js
@@ -113,7 +113,7 @@ define([
'single': {
width: 10,
height: 12,
- render: BaseTheme.renderHorizArrowHead,
+ render: BaseTheme.renderArrowHead,
attrs: {
'fill': '#000000',
'stroke': '#000000',
@@ -124,7 +124,7 @@ define([
'double': {
width: 10,
height: 12,
- render: BaseTheme.renderHorizArrowHead,
+ render: BaseTheme.renderArrowHead,
attrs: {
'fill': 'none',
'stroke': '#000000',
diff --git a/scripts/sequence/themes/Monospace.js b/scripts/sequence/themes/Monospace.js
index c0afcc0..521fdc6 100644
--- a/scripts/sequence/themes/Monospace.js
+++ b/scripts/sequence/themes/Monospace.js
@@ -114,7 +114,7 @@ define([
'single': {
width: 4,
height: 8,
- render: BaseTheme.renderHorizArrowHead,
+ render: BaseTheme.renderArrowHead,
attrs: {
'fill': '#000000',
'stroke-width': 0,
@@ -124,7 +124,7 @@ define([
'double': {
width: 3,
height: 6,
- render: BaseTheme.renderHorizArrowHead,
+ render: BaseTheme.renderArrowHead,
attrs: {
'fill': 'none',
'stroke': '#000000',
diff --git a/scripts/sequence/themes/Sketch.js b/scripts/sequence/themes/Sketch.js
index 935b216..b21de35 100644
--- a/scripts/sequence/themes/Sketch.js
+++ b/scripts/sequence/themes/Sketch.js
@@ -658,23 +658,23 @@ define([
return {shape};
}
- renderFlatConnector(attrs, {x1, dx1, x2, dx2, y}) {
+ renderFlatConnector(attrs, {x1, y1, x2, y2}) {
const ln = this.lineNodes(
- {x: x1 + dx1, y},
- {x: x2 + dx2, y},
+ {x: x1, y: y1},
+ {x: x2, y: y2},
{varX: 0.3}
);
return {
shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)),
- p1: {x: ln.p1.x - dx1, y: ln.p1.y},
- p2: {x: ln.p2.x - dx2, y: ln.p2.y},
+ p1: ln.p1,
+ p2: ln.p2,
};
}
- renderRevConnector(attrs, {xL, dx1, dx2, y1, y2, xR}) {
- const variance = Math.min((xR - xL) * 0.06, 3);
- const overshoot = Math.min((xR - xL) * 0.5, 6);
- const p1x = xL + dx1 + this.vary(variance, -1);
+ renderRevConnector(attrs, {x1, y1, x2, y2, xR}) {
+ const variance = Math.min((xR - x1) * 0.06, 3);
+ const overshoot = Math.min((xR - x1) * 0.5, 6);
+ const p1x = x1 + this.vary(variance, -1);
const p1y = y1 + this.vary(variance, -1);
const b1x = xR - overshoot * this.vary(0.2, 1);
const b1y = y1 - this.vary(1, 2);
@@ -682,7 +682,7 @@ define([
const p2y = y1 + this.vary(1, 1);
const b2x = xR;
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);
return {
@@ -696,41 +696,21 @@ define([
',' + p3x + ' ' + p3y
),
}, attrs)),
- p1: {x: p1x - dx1, y: p1y},
- p2: {x: p3x - dx2, y: p3y},
+ p1: {x: p1x, y: p1y},
+ 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 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 y2v = y2 + this.vary(1);
return {
shape: svg.make('path', Object.assign({
d: new SVGShapes.PatternedLine(this.wave)
- .move(x1v + dx1, y1v)
- .line(xR, y1)
- .arc(xR, (y1 + y2) / 2, Math.PI)
- .line(x2v + dx2, y2v)
+ .move(x1v, y1v)
+ .line(x2v, y2v)
.cap()
.asPath(),
}, attrs)),
@@ -739,17 +719,41 @@ define([
};
}
- renderArrowHead(attrs, {x, y, dx, dy}) {
- const w = dx * this.vary(0.2, 1);
- const h = dy * this.vary(0.3, 1);
+ renderRevConnectorWave(attrs, {x1, y1, x2, y2, xR}) {
+ const x1v = x1 + this.vary(0.3);
+ 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(
- {x: x + w, y: y - h},
+ {x: x + wx - hx, y: y + wy - hy},
{x, y},
{var1: 2.0, var2: 0.2}
);
const l2 = this.lineNodes(
l1.p2,
- {x: x + w, y: y + h},
+ {x: x + wx + hx, y: y + wy + hy},
{var1: 0, var2: 2.0, move: false}
);
const l3 = (attrs.fill === 'none') ? {nodes: ''} : this.lineNodes(
diff --git a/scripts/svg/PatternedLine.js b/scripts/svg/PatternedLine.js
index fffbbca..7a9d262 100644
--- a/scripts/svg/PatternedLine.js
+++ b/scripts/svg/PatternedLine.js
@@ -73,7 +73,7 @@ define(() => {
(cx - this.x) * (cx - this.x) +
(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 nextY = cy - Math.cos(theta1 + theta) * radius;
@@ -93,8 +93,8 @@ define(() => {
this.points.push(
this.x + ' ' + this.y +
'A' + radius + ' ' + radius + ' 0 ' +
+ ((Math.abs(theta) >= Math.PI) ? '1 ' : '0 ') +
((theta < 0) ? '0 ' : '1 ') +
- '1 ' +
nextX + ' ' + nextY
);
this.disconnect = 0;
diff --git a/scripts/svg/PatternedLine_spec.js b/scripts/svg/PatternedLine_spec.js
index e69d549..32543e7 100644
--- a/scripts/svg/PatternedLine_spec.js
+++ b/scripts/svg/PatternedLine_spec.js
@@ -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', () => {
const ln = new PatternedLine()
.move(10, 20)
diff --git a/styles/editor.css b/styles/editor.css
index ab24915..c534773 100644
--- a/styles/editor.css
+++ b/styles/editor.css
@@ -153,17 +153,17 @@ html, body {
height: 100%;
}
-.pane-view .region:hover rect {
+.pane-view .region:hover .vis {
stroke-width: 5px;
stroke: rgba(255, 255, 0, 0.5);
}
-.pane-view .region.focus rect {
+.pane-view .region.focus .vis {
stroke-width: 5px;
stroke: rgba(255, 128, 0, 0.5);
}
-.pane-view .region.focus:hover rect {
+.pane-view .region.focus:hover .vis {
stroke-width: 5px;
stroke: rgba(255, 192, 0, 0.5);
}