Add initial support for asynchronous action blocks [#12]

This commit is contained in:
David Evans 2017-10-28 17:43:37 +01:00
parent 4772b16783
commit 912c9dbb64
7 changed files with 334 additions and 144 deletions

View File

@ -150,6 +150,39 @@ Foo -> Bar
Bar -> Baz Bar -> Baz
``` ```
### Simultaneous Actions (Beta!)
<img src="screenshots/SimultaneousActions.png" alt="Simultaneous Actions preview" width="200" align="right" />
This is a work-in-progress feature. There are situations where this can
lead to [ugly / unreadable overlapping content](https://github.com/davidje13/SequenceDiagram/issues/13).
```
begin A, B, C, D
A -> C
# Define a marker which can be returned to later
some primary process:
A -> B
B -> A
A -> B
B -> A
# Return to the defined marker
# (should be interpreted as no-higher-then the marker; may still be
# pushed down to keep relative action ordering consistent)
simultaneously with some primary process:
C -> D
D -> C
end D
C -> A
# The marker name is optional; using "simultaneously:" with no marker
# will jump to the top of the entire sequence.
```
## DSL Basics ## DSL Basics
Comments begin with a `#` and end at the next newline: Comments begin with a `#` and end at the next newline:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -17,10 +17,13 @@ define(['core/ArrayUtilities'], (array) => {
this.agents = []; this.agents = [];
this.blockCount = 0; this.blockCount = 0;
this.nesting = []; this.nesting = [];
this.markers = new Set();
this.currentSection = null; this.currentSection = null;
this.currentNest = null; this.currentNest = null;
this.stageHandlers = { this.stageHandlers = {
'mark': this.handleMark.bind(this),
'async': this.handleAsync.bind(this),
'agent define': this.handleAgentDefine.bind(this), 'agent define': this.handleAgentDefine.bind(this),
'agent begin': this.handleAgentBegin.bind(this), 'agent begin': this.handleAgentBegin.bind(this),
'agent end': this.handleAgentEnd.bind(this), 'agent end': this.handleAgentEnd.bind(this),
@ -83,6 +86,7 @@ define(['core/ArrayUtilities'], (array) => {
agents: filteredAgents, agents: filteredAgents,
mode, mode,
}); });
this.currentNest.hasContent = true;
} }
array.mergeSets(this.currentNest.agents, filteredAgents); array.mergeSets(this.currentNest.agents, filteredAgents);
array.mergeSets(this.agents, filteredAgents); array.mergeSets(this.agents, filteredAgents);
@ -100,6 +104,7 @@ define(['core/ArrayUtilities'], (array) => {
}; };
this.currentNest = { this.currentNest = {
agents, agents,
hasContent: false,
stage: { stage: {
type: 'block', type: 'block',
sections: [this.currentSection], sections: [this.currentSection],
@ -114,6 +119,18 @@ define(['core/ArrayUtilities'], (array) => {
return {agents, stages}; return {agents, stages};
} }
handleMark(stage) {
this.markers.add(stage.name);
this.currentSection.stages.push(stage);
}
handleAsync(stage) {
if(stage.target !== '' && !this.markers.has(stage.target)) {
throw new Error('Unknown marker: ' + stage.target);
}
this.currentSection.stages.push(stage);
}
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
array.mergeSets(this.currentNest.agents, agents); array.mergeSets(this.currentNest.agents, agents);
array.mergeSets(this.agents, agents); array.mergeSets(this.agents, agents);
@ -149,10 +166,10 @@ define(['core/ArrayUtilities'], (array) => {
if(this.nesting.length <= 1) { if(this.nesting.length <= 1) {
throw new Error('Invalid block nesting'); throw new Error('Invalid block nesting');
} }
const {stage, agents} = this.nesting.pop(); const {hasContent, stage, agents} = this.nesting.pop();
this.currentNest = array.last(this.nesting); this.currentNest = array.last(this.nesting);
this.currentSection = array.last(this.currentNest.stage.sections); this.currentSection = array.last(this.currentNest.stage.sections);
if(stage.sections.some((section) => section.stages.length > 0)) { if(hasContent) {
array.mergeSets(this.currentNest.agents, agents); array.mergeSets(this.currentNest.agents, agents);
array.mergeSets(this.agents, agents); array.mergeSets(this.agents, agents);
this.addBounds( this.addBounds(
@ -162,14 +179,18 @@ define(['core/ArrayUtilities'], (array) => {
agents agents
); );
this.currentSection.stages.push(stage); this.currentSection.stages.push(stage);
this.currentNest.hasContent = true;
} }
} }
handleUnknownStage(stage) { handleUnknownStage(stage) {
this.setAgentVis(stage.agents, true, 'box'); if(stage.agents) {
this.setAgentVis(stage.agents, true, 'box');
array.mergeSets(this.currentNest.agents, stage.agents);
array.mergeSets(this.agents, stage.agents);
}
this.currentSection.stages.push(stage); this.currentSection.stages.push(stage);
array.mergeSets(this.currentNest.agents, stage.agents); this.currentNest.hasContent = true;
array.mergeSets(this.agents, stage.agents);
} }
handleStage(stage) { handleStage(stage) {
@ -183,6 +204,7 @@ define(['core/ArrayUtilities'], (array) => {
generate({stages, meta = {}}) { generate({stages, meta = {}}) {
this.agentStates.clear(); this.agentStates.clear();
this.markers.clear();
this.agents.length = 0; this.agents.length = 0;
this.blockCount = 0; this.blockCount = 0;
this.nesting.length = 0; this.nesting.length = 0;

View File

@ -31,6 +31,26 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(sequence.agents).toEqual(['[', ']']); expect(sequence.agents).toEqual(['[', ']']);
}); });
it('passes marks and async through', () => {
const sequence = generator.generate({stages: [
{type: 'mark', name: 'foo'},
{type: 'async', target: 'foo'},
{type: 'async', target: ''},
]});
expect(sequence.stages).toEqual([
{type: 'mark', name: 'foo'},
{type: 'async', target: 'foo'},
{type: 'async', target: ''},
]);
});
it('rejects attempts to jump to markers not yet defined', () => {
expect(() => generator.generate({stages: [
{type: 'async', target: 'foo'},
{type: 'mark', name: 'foo'},
]})).toThrow();
});
it('returns aggregated agents', () => { it('returns aggregated agents', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']}, {type: '->', agents: ['A', 'B']},
@ -369,10 +389,11 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(sequence.stages).toEqual([]); expect(sequence.stages).toEqual([]);
}); });
it('removes blocks which only contain define statements', () => { it('removes blocks containing only define statements / markers', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: AGENT_DEFINE, agents: ['A']}, {type: AGENT_DEFINE, agents: ['A']},
{type: 'mark', name: 'foo'},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});

View File

@ -1,4 +1,4 @@
define(() => { define(['core/ArrayUtilities'], (array) => {
'use strict'; 'use strict';
function execAt(str, reg, i) { function execAt(str, reg, i) {
@ -160,124 +160,162 @@ define(() => {
return list; return list;
} }
function parseBlockCommand(line) { const PARSERS = [
if(line[0] === 'end' && line.length === 1) { (line, meta) => { // title
return {type: 'block end'}; if(line[0] !== 'title') {
} return null;
const type = BLOCK_TYPES[line[0]];
if(!type) {
return null;
}
let skip = 1;
if(line.length > skip) {
skip = skipOver(line, skip, type.skip, 'Invalid block command');
}
skip = skipOver(line, skip, [':']);
return {
type: type.type,
mode: type.mode,
label: line.slice(skip).join(' '),
};
}
function parseAgentCommand(line) {
const type = AGENT_MANIPULATION_TYPES[line[0]];
if(!type) {
return null;
}
if(line.length <= 1) {
return null;
}
return Object.assign({
agents: parseCommaList(line.slice(1)),
}, type);
}
function parseNote(line) {
const mode = NOTE_TYPES[line[0]];
const labelSplit = line.indexOf(':');
if(!mode || labelSplit === -1) {
return null;
}
const type = mode.types[line[1]];
if(!type) {
return null;
}
let skip = 2;
skip = skipOver(line, skip, type.skip);
const agents = parseCommaList(line.slice(skip, labelSplit));
if(
agents.length < type.min ||
(type.max !== null && agents.length > type.max)
) {
throw new Error('Invalid ' + line[0] + ': ' + line.join(' '));
}
return {
type: type.type,
agents,
mode: mode.mode,
label: line.slice(labelSplit + 1).join(' '),
};
}
function parseConnection(line) {
let labelSplit = line.indexOf(':');
if(labelSplit === -1) {
labelSplit = line.length;
}
let typeSplit = -1;
let options = null;
for(let j = 0; j < line.length; ++ j) {
const opts = CONNECTION_TYPES[line[j]];
if(opts) {
typeSplit = j;
options = opts;
break;
} }
}
if(typeSplit <= 0 || typeSplit >= labelSplit - 1) {
return null;
}
return Object.assign({
type: 'connection',
agents: [
line.slice(0, typeSplit).join(' '),
line.slice(typeSplit + 1, labelSplit).join(' '),
],
label: line.slice(labelSplit + 1).join(' '),
}, options);
}
function parseMeta(line, meta) {
if(line[0] === 'title') {
meta.title = line.slice(1).join(' '); meta.title = line.slice(1).join(' ');
return true; return true;
} },
if(line[0] === 'terminators') {
(line, meta) => { // terminators
if(line[0] !== 'terminators') {
return null;
}
if(TERMINATOR_TYPES.indexOf(line[1]) === -1) { if(TERMINATOR_TYPES.indexOf(line[1]) === -1) {
throw new Error('Unrecognised termination: ' + line.join(' ')); throw new Error('Unknown termination: ' + line.join(' '));
} }
meta.terminators = line[1]; meta.terminators = line[1];
return true; return true;
} },
return false;
} (line) => { // block
if(line[0] === 'end' && line.length === 1) {
return {type: 'block end'};
}
const type = BLOCK_TYPES[line[0]];
if(!type) {
return null;
}
let skip = 1;
if(line.length > skip) {
skip = skipOver(line, skip, type.skip, 'Invalid block command');
}
skip = skipOver(line, skip, [':']);
return {
type: type.type,
mode: type.mode,
label: line.slice(skip).join(' '),
};
},
(line) => { // agent
const type = AGENT_MANIPULATION_TYPES[line[0]];
if(!type) {
return null;
}
if(line.length <= 1) {
return null;
}
return Object.assign({
agents: parseCommaList(line.slice(1)),
}, type);
},
(line) => { // async
if(line[0] !== 'simultaneously') {
return null;
}
if(array.last(line) !== ':') {
return null;
}
let target = '';
if(line.length > 2) {
if(line[1] !== 'with') {
return null;
}
target = line.slice(2, line.length - 1).join(' ');
}
return {
type: 'async',
target,
};
},
(line) => { // note
const mode = NOTE_TYPES[line[0]];
const labelSplit = line.indexOf(':');
if(!mode || labelSplit === -1) {
return null;
}
const type = mode.types[line[1]];
if(!type) {
return null;
}
let skip = 2;
skip = skipOver(line, skip, type.skip);
const agents = parseCommaList(line.slice(skip, labelSplit));
if(
agents.length < type.min ||
(type.max !== null && agents.length > type.max)
) {
throw new Error('Invalid ' + line[0] + ': ' + line.join(' '));
}
return {
type: type.type,
agents,
mode: mode.mode,
label: line.slice(labelSplit + 1).join(' '),
};
},
(line) => { // connection
let labelSplit = line.indexOf(':');
if(labelSplit === -1) {
labelSplit = line.length;
}
let typeSplit = -1;
let options = null;
for(let j = 0; j < line.length; ++ j) {
const opts = CONNECTION_TYPES[line[j]];
if(opts) {
typeSplit = j;
options = opts;
break;
}
}
if(typeSplit <= 0 || typeSplit >= labelSplit - 1) {
return null;
}
return Object.assign({
type: 'connection',
agents: [
line.slice(0, typeSplit).join(' '),
line.slice(typeSplit + 1, labelSplit).join(' '),
],
label: line.slice(labelSplit + 1).join(' '),
}, options);
},
(line) => { // marker
if(line.length < 2 || array.last(line) !== ':') {
return null;
}
return {
type: 'mark',
name: line.slice(0, line.length - 1).join(' '),
};
},
];
function parseLine(line, {meta, stages}) { function parseLine(line, {meta, stages}) {
if(parseMeta(line, meta)) { let stage = null;
return; for(let i = 0; i < PARSERS.length; ++ i) {
stage = PARSERS[i](line, meta);
if(stage) {
break;
}
} }
const stage = (
parseBlockCommand(line) ||
parseAgentCommand(line) ||
parseNote(line) ||
parseConnection(line)
);
if(!stage) { if(!stage) {
throw new Error('Unrecognised command: ' + line.join(' ')); throw new Error('Unrecognised command: ' + line.join(' '));
} }
stages.push(stage); if(typeof stage === 'object') {
stages.push(stage);
}
} }
return class Parser { return class Parser {

View File

@ -355,6 +355,30 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
]); ]);
}); });
it('converts markers', () => {
const parsed = parser.parse('abc:');
expect(parsed.stages).toEqual([{
type: 'mark',
name: 'abc',
}]);
});
it('converts "simultaneously" flow commands', () => {
const parsed = parser.parse('simultaneously:');
expect(parsed.stages).toEqual([{
type: 'async',
target: '',
}]);
});
it('converts named "simultaneously" flow commands', () => {
const parsed = parser.parse('simultaneously with abc:');
expect(parsed.stages).toEqual([{
type: 'async',
target: 'abc',
}]);
});
it('converts conditional blocks', () => { it('converts conditional blocks', () => {
const parsed = parser.parse( const parsed = parser.parse(
'if something happens\n' + 'if something happens\n' +

View File

@ -61,12 +61,14 @@ define([
}; };
this.separationAction = { this.separationAction = {
'mark': this.separationMark.bind(this),
'async': this.separationAsync.bind(this),
'agent begin': this.separationAgent.bind(this), 'agent begin': this.separationAgent.bind(this),
'agent end': this.separationAgent.bind(this), 'agent end': this.separationAgent.bind(this),
'connection': this.separationConnection.bind(this), 'connection': this.separationConnection.bind(this),
'note over': this.separationNoteOver.bind(this), 'note over': this.separationNoteOver.bind(this),
'note left': this.separationNoteLeft.bind(this), 'note left': this.separationNoteSide.bind(this, false),
'note right': this.separationNoteRight.bind(this), 'note right': this.separationNoteSide.bind(this, true),
'note between': this.separationNoteBetween.bind(this), 'note between': this.separationNoteBetween.bind(this),
}; };
@ -78,6 +80,8 @@ define([
}; };
this.renderAction = { this.renderAction = {
'mark': this.renderMark.bind(this),
'async': this.renderAsync.bind(this),
'agent begin': this.renderAgentBegin.bind(this), 'agent begin': this.renderAgentBegin.bind(this),
'agent end': this.renderAgentEnd.bind(this), 'agent end': this.renderAgentEnd.bind(this),
'connection': this.renderConnection.bind(this), 'connection': this.renderConnection.bind(this),
@ -104,6 +108,7 @@ define([
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.marks = new Map();
this.theme = theme; this.theme = theme;
this.currentSequence = null; this.currentSequence = null;
this.buildStaticElements(); this.buildStaticElements();
@ -180,6 +185,12 @@ define([
}); });
} }
separationMark() {
}
separationAsync() {
}
separationAgentCapBox({label}) { separationAgentCapBox({label}) {
const config = this.theme.agentCap.box; const config = this.theme.agentCap.box;
const width = ( const width = (
@ -300,39 +311,23 @@ define([
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
} }
separationNoteLeft({agents, mode, label}) { separationNoteSide(isRight, {agents, mode, label}) {
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {left} = this.findExtremes(agents); const {left, right} = this.findExtremes(agents);
const width = (
this.sizer.measure(config.labelAttrs, label).width +
config.padding.left +
config.padding.right +
config.margin.left +
config.margin.right
);
const agentSpaces = new Map(); const agentSpaces = new Map();
agentSpaces.set(left, { if(isRight) {
left: ( agentSpaces.set(right, {left: 0, right: width});
this.sizer.measure(config.labelAttrs, label).width + } else {
config.padding.left + agentSpaces.set(left, {left: width, right: 0});
config.padding.right + }
config.margin.left +
config.margin.right
),
right: 0,
});
this.addSeparations(this.visibleAgents, agentSpaces);
}
separationNoteRight({agents, mode, label}) {
const config = this.theme.note[mode];
const {right} = this.findExtremes(agents);
const agentSpaces = new Map();
agentSpaces.set(right, {
left: 0,
right: (
this.sizer.measure(config.labelAttrs, label).width +
config.padding.left +
config.padding.right +
config.margin.left +
config.margin.right
),
});
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
} }
@ -378,6 +373,18 @@ define([
this.separationAction[stage.type](stage); this.separationAction[stage.type](stage);
} }
renderMark({name}) {
this.marks.set(name, this.currentY);
}
renderAsync({target}) {
if(target) {
this.currentY = this.marks.get(target) || 0;
} else {
this.currentY = 0;
}
}
renderAgentCapBox({x, label}) { renderAgentCapBox({x, label}) {
const config = this.theme.agentCap.box; const config = this.theme.agentCap.box;
const {height} = SVGShapes.renderBoxedText(label, { const {height} = SVGShapes.renderBoxedText(label, {
@ -449,7 +456,30 @@ define([
}; };
} }
checkAgentRange(agents) {
const {left, right} = this.findExtremes(agents);
const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x;
this.agentInfos.forEach((agentInfo) => {
if(agentInfo.x >= leftX && agentInfo.x <= rightX) {
this.currentY = Math.max(this.currentY, agentInfo.latestY);
}
});
}
markAgentRange(agents) {
const {left, right} = this.findExtremes(agents);
const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x;
this.agentInfos.forEach((agentInfo) => {
if(agentInfo.x >= leftX && agentInfo.x <= rightX) {
agentInfo.latestY = this.currentY;
}
});
}
renderAgentBegin({mode, agents}) { renderAgentBegin({mode, agents}) {
this.checkAgentRange(agents);
let maxHeight = 0; let maxHeight = 0;
agents.forEach((agent) => { agents.forEach((agent) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(agent);
@ -458,9 +488,11 @@ define([
agentInfo.latestYStart = this.currentY + shifts.lineBottom; agentInfo.latestYStart = this.currentY + shifts.lineBottom;
}); });
this.currentY += maxHeight + this.theme.actionMargin; this.currentY += maxHeight + this.theme.actionMargin;
this.markAgentRange(agents);
} }
renderAgentEnd({mode, agents}) { renderAgentEnd({mode, agents}) {
this.checkAgentRange(agents);
let maxHeight = 0; let maxHeight = 0;
agents.forEach((agent) => { agents.forEach((agent) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(agent);
@ -477,6 +509,7 @@ define([
agentInfo.latestYStart = null; agentInfo.latestYStart = null;
}); });
this.currentY += maxHeight + this.theme.actionMargin; this.currentY += maxHeight + this.theme.actionMargin;
this.markAgentRange(agents);
} }
renderSelfConnection({label, agents, line, left, right}) { renderSelfConnection({label, agents, line, left, right}) {
@ -609,11 +642,13 @@ define([
} }
renderConnection(stage) { renderConnection(stage) {
this.checkAgentRange(stage.agents);
if(stage.agents[0] === stage.agents[1]) { if(stage.agents[0] === stage.agents[1]) {
this.renderSelfConnection(stage); this.renderSelfConnection(stage);
} else { } else {
this.renderSimpleConnection(stage); this.renderSimpleConnection(stage);
} }
this.markAgentRange(stage.agents);
} }
renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) { renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) {
@ -679,6 +714,7 @@ define([
} }
renderNoteOver({agents, mode, label}) { renderNoteOver({agents, mode, label}) {
this.checkAgentRange(agents);
const config = this.theme.note[mode]; const config = this.theme.note[mode];
if(agents.length > 1) { if(agents.length > 1) {
@ -691,25 +727,31 @@ define([
const xMid = this.agentInfos.get(agents[0]).x; const xMid = this.agentInfos.get(agents[0]).x;
this.renderNote({xMid}, 'middle', mode, label); this.renderNote({xMid}, 'middle', mode, label);
} }
this.markAgentRange(agents);
} }
renderNoteLeft({agents, mode, label}) { renderNoteLeft({agents, mode, label}) {
this.checkAgentRange(agents);
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {left} = this.findExtremes(agents); const {left} = this.findExtremes(agents);
const x1 = this.agentInfos.get(left).x - config.margin.right; const x1 = this.agentInfos.get(left).x - config.margin.right;
this.renderNote({x1}, 'end', mode, label); this.renderNote({x1}, 'end', mode, label);
this.markAgentRange(agents);
} }
renderNoteRight({agents, mode, label}) { renderNoteRight({agents, mode, label}) {
this.checkAgentRange(agents);
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {right} = this.findExtremes(agents); const {right} = this.findExtremes(agents);
const x0 = this.agentInfos.get(right).x + config.margin.left; const x0 = this.agentInfos.get(right).x + config.margin.left;
this.renderNote({x0}, 'start', mode, label); this.renderNote({x0}, 'start', mode, label);
this.markAgentRange(agents);
} }
renderNoteBetween({agents, mode, label}) { renderNoteBetween({agents, mode, label}) {
this.checkAgentRange(agents);
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agents);
const xMid = ( const xMid = (
this.agentInfos.get(left).x + this.agentInfos.get(left).x +
@ -717,16 +759,20 @@ define([
) / 2; ) / 2;
this.renderNote({xMid}, 'middle', mode, label); this.renderNote({xMid}, 'middle', mode, label);
this.markAgentRange(agents);
} }
renderBlockBegin(scope) { renderBlockBegin(scope, {left, right}) {
this.checkAgentRange([left, right]);
this.currentY += this.theme.block.margin.top; this.currentY += this.theme.block.margin.top;
scope.y = this.currentY; scope.y = this.currentY;
scope.first = true; scope.first = true;
this.markAgentRange([left, right]);
} }
renderSectionBegin(scope, {left, right}, {mode, label}) { renderSectionBegin(scope, {left, right}, {mode, label}) {
this.checkAgentRange([left, right]);
const config = this.theme.block; const config = this.theme.block;
const agentInfoL = this.agentInfos.get(left); const agentInfoL = this.agentInfos.get(left);
const agentInfoR = this.agentInfos.get(right); const agentInfoR = this.agentInfos.get(right);
@ -767,12 +813,14 @@ define([
Math.max(modeRender.height, labelRender.height) + Math.max(modeRender.height, labelRender.height) +
config.section.padding.top config.section.padding.top
); );
this.markAgentRange([left, right]);
} }
renderSectionEnd(/*scope, block, section*/) { renderSectionEnd(/*scope, block, section*/) {
} }
renderBlockEnd(scope, {left, right}) { renderBlockEnd(scope, {left, right}) {
this.checkAgentRange([left, right]);
const config = this.theme.block; const config = this.theme.block;
this.currentY += config.section.padding.bottom; this.currentY += config.section.padding.bottom;
@ -786,6 +834,7 @@ define([
}, config.boxAttrs))); }, config.boxAttrs)));
this.currentY += config.margin.bottom + this.theme.actionMargin; this.currentY += config.margin.bottom + this.theme.actionMargin;
this.markAgentRange([left, right]);
} }
addAction(stage) { addAction(stage) {
@ -835,6 +884,7 @@ define([
index, index,
x: null, x: null,
latestYStart: null, latestYStart: null,
latestY: 0,
separations: new Map(), separations: new Map(),
}); });
}); });
@ -884,6 +934,7 @@ define([
svg.empty(this.sections); svg.empty(this.sections);
svg.empty(this.actionShapes); svg.empty(this.actionShapes);
svg.empty(this.actionLabels); svg.empty(this.actionLabels);
this.marks.clear();
this.title.set({ this.title.set({
attrs: this.theme.titleAttrs, attrs: this.theme.titleAttrs,
@ -896,6 +947,7 @@ define([
this.currentY = 0; this.currentY = 0;
traverse(sequence.stages, this.renderTraversalFns); traverse(sequence.stages, this.renderTraversalFns);
this.checkAgentRange(['[', ']']);
const stagesHeight = Math.max( const stagesHeight = Math.max(
this.currentY - this.theme.actionMargin, this.currentY - this.theme.actionMargin,