From 912c9dbb649b047ef30750b61dedad862d48e5e2 Mon Sep 17 00:00:00 2001 From: David Evans Date: Sat, 28 Oct 2017 17:43:37 +0100 Subject: [PATCH] Add initial support for asynchronous action blocks [#12] --- README.md | 33 ++++ screenshots/SimultaneousActions.png | Bin 0 -> 5620 bytes scripts/sequence/Generator.js | 32 +++- scripts/sequence/Generator_spec.js | 23 ++- scripts/sequence/Parser.js | 248 ++++++++++++++++------------ scripts/sequence/Parser_spec.js | 24 +++ scripts/sequence/Renderer.js | 118 +++++++++---- 7 files changed, 334 insertions(+), 144 deletions(-) create mode 100644 screenshots/SimultaneousActions.png diff --git a/README.md b/README.md index 25a5e7e..5e6b41a 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,39 @@ Foo -> Bar Bar -> Baz ``` +### Simultaneous Actions (Beta!) + +Simultaneous Actions preview + +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 Comments begin with a `#` and end at the next newline: diff --git a/screenshots/SimultaneousActions.png b/screenshots/SimultaneousActions.png new file mode 100644 index 0000000000000000000000000000000000000000..0d44c5b8073e54bf6661e7d90c6d405557f8e191 GIT binary patch literal 5620 zcmb_gdpuNI`==6-OXnOTgi57Mqf&U0OB8ZRO{L7Z9Mp`M&2fn|E=g1_r8Fe>qfzP% zO=GrPrgRy%L6cm@Y|P1Rh~W*5+{U}dIi1dXdOx4vKfljsukUBA@AEust>;<$S$pku z{pb-}d091C2?+`L!*&PJ5)v!ua9JU<3TBA7MQI5M<*LI651hEb74#7AYMhU%`{ca) zz(KuND)MVob#HD=Tw%|8y>(5v>~`O%M0R4LN}}ZY zV4w9@mAurp!VV>YmfJ{&;rj;6EK6$clEWe$4BzYTI_^K-gxo)HtZ?gZ*m!=zYKcC& z^>9i3I4cUbSgn)SI*H!)rNE_lSw45oMQ z_N_AHHQBB^yhk(p5!a_mSW)!4wIA#Qdo--2iAtcKxBw@Ou9s1RgV z`&ZaD*L$TT1Fw&phS%e9_*kE4sop$|sqi?kXR;(ai&^2vR(u4Onzy_SAm1#)AGpCn z>X%GcWDHr)`V=9w-m`2x_`n`u>}-s;HlI0D65do= za~wIl@MSDV-62WF2)tqPNk{PbT#pzD^5q`qWjrFBJ8V*y}R4wX`QAYEhrJU8wjrBI2dJma(}CNq+&5 ze)mM7QJH+xyzAM6I<*{btV2~;vyd6PRCu6s%kJu+t~w$8$1&1_TLgL2IZKd9bLYhI%iaLwMDvZ za_Qxmh5_#-A{wh=p_}_^_UCP9+!XXg0}J~F4>fP;ByVg$F?-+G^JjN8R83YAvIMj+ z&E3ys4I6JUMdfNjcP^(*uesA6uX6aQb5jJUv(hdIDA0ZZRk56q}NP!)tT6?ITgF1 zqi>CT_msh&T_H80%{&D0MGLxE5FW1bm`coWX*aBoWj+-JhTljJV56$cLJ|lTtiJuJ zNc#-lmicF4+Rj-1#Jh=OPL0P`9w)|(Se5SLcW1kETU4hpp75|OU#fS`57lOSM&i6g zg=E_L3$tlZ^$I}6;B(8H)kR@P&Mjpx_?u(6H&BOO%U8BDEOeCT@!+&!PjjM!R9;Co z_rcSRa`0nDK0O=WYK+DzCfOSeDBwNDg9xWWPf%wFKD@x$;cWma9j% z&LH0AJL)nTGung&6A0o}{%<701X8nqb_((UbWE@4s$L+Hi(Q@l z<~wVY>7)Fz4OO=V@0tFQ6$i>6Z7u1nS5IWjlBe29^Ik#kJ`607tRJ?{OmrqKu2d7= zHkWr0)Vwq^%c4vlQ&0EkAJun0ed1~UGY=nc{}|QYr=dPGL9?vgH$6^V-1};!dlL3# zYqneD(h*%$ryZb`8xck2ScH<6&aDjGW%gdalC*Yb({DNFu_kdr#zmaGjN?~lH$?@E zxW;L3uvB)tE0?&KMvH9|mhuq0yv%~`;&+uJJ6aIw9>RO1ijto)3)N!A!GBj zwN2?VQUUDdM$b~ih|Qjj z<4oI8ehw{a2zAn<9qAA^;)A{DjEAUr+Qq2BNbTA0t@&+w zx;?4H;U_mWPoEEo3%K3Xpj;7IFJxA0s}C{Abvd_C7_$eR8NKZW#>gzb!>s$r)z3d> zW_7M@tYvm}e;{p%*w0fBzT$qzhvq7(oMvQY=jN!-`c&>&UGgv|8_bSng<-~U(VXyXw0H{6%C$>YYChQ{Y9B(5?Q}4 z>`cp!Tv_bUzc=vMylZOx5VQA|a`fW%7mi=zUB~A0FTbCvaCz~sFZ%t#FY}?QjEa4E z&GF=LE$q-_b)DDV&HOBH zG4<^!RV_iy)rK#c+L6tIfJ_pvd@iM%RjWu$&bd80b+Cmnt#Li&u;7L(w=1CJON+Dm zKyF{m`wq{kBYNAOJQaM*&W!4Q`QlXwWESi^&e|H`S%kak*gRdWgCCq**wcxtkB}&qO2xqNeFJ8` zNxn8(3%2+B{nz|Uf$qs;gOtFNK%EdP>eo#On zXETUS`U5U=>`~rHe}NqQ@xF#yhHI3z&EPlK_?OADaiUnsf4Hc-yAQxIIY&A!w;L9= z+j5=?_EAaPoTnj4hbj%q%Pv$=(7w){EQ4QcAK!d(7GjXB zh=(8rc{v#w6r=qW+a6_=^cTokA?Y%xZy*D^QML>l%h1R&7?RkV#c=sA6R~kh=&$V7 zmGf&TZJ=pcp5dyCN?fG;0l#>}6&Jq|wF2^dI@|{cy|%Gbge&mB%|Dmrq?7)N;0_WIfjyWl=j`uVH-LFUudt+{+bU7>px( zPC=czx$mBXgLRy+F1<7|Sg})s?T%HGL}z6WX=Rs~FlAD?0|VKZ562uZqNJYj=O`D5 zIr}rY&WV00wVw<&9C`E;WQt`hTJ7nqQPE`zGsNRG<;>=^C89!2!PAyXSU5Xf2UQ&<_h4Bt%t%Z_XEMi^=hr#5%I4pFHNXZ>*Htj*Uyw!iEjId ztVqRF2AXP16q6xb5}av0U?%4>{0dLMke?F7Io)^V=O-t^Y@%~fWCWEw zUdwN-HiL!|%##JG#+$H19ikKp?9GV~N1^j=Le}W~P7ptEWlIAqqyp}u4XUVkM4L%? zZbFMPFKfsPzP3z;7(clNEbY|3V_g+8o`HFTpaSQ7$u0zD`dlq&SU|#1m9aLO(Bt{d zpnk4R8XM7+GPd_(*LOJ01f1q86A)= z4?t+5^EmKH*q=!4v9TCtaXS801~>>FV;D}w7jVSu7ui>-l=Hr*jl0{5Y%F<9a0s49 zg2_9XVH&5yT&&tb@vyuQLRQttDIHl|s04k!0y2sjrR`Xf_4IIdscN!4p;U{^G2C2+ zP*TEF{sf*N!GiLw}vY*p?;r_3ri;@MuvqhET*0;os zB;fUOE-tD-weickC{P~uMwY^E^`ru4;2`-H zsD~qXxRvZg2t-9qduH+$md+GQT@+l&#>@?)3Wej9XgPL}cThy}%MtvEn@|f1ij^_L)_>w)o{K``(*=LIQbmV0A7L~d4XQxq~5cX>wxKTcu)ab z-hl4KIW9ZO{0C5AC!}U zcF07janKgGCJE6QEkR$21!6xpqCQ%VTiB4G8D!`Z3BgHIYI&JFnzJsjT-+5H}t$kRlxh~Q1Z7ytkzZVN{aUaJ&jCJGjWrJt0ln+*lG{l?ZiJ7D0+|Qf^Hw-XwvBdf3rCB?)-REa8}o zODd*7>b{Gwq`3Tp{wjj$uNGn|m1{tc5q~|dVi-N|GhgFSd9Uq2)csY(8o#(Q>wwme zKz|>H^Ts78B+gWPoyVBowDkL>wlHYcxbjtDdf7t82a0LP>7TvwCxsU1%WXIIgEz}A z63f2anz}~m0}tA#a@()NUrVuz-g4}@KPzJQ{|tdx_Ej#{`7Scvqd<^EvEUE^J}Lk6 z2>kWz{Qr88{^KzEe{~s_Oi5dlnYu<)fsA;zmx4 { this.agents = []; this.blockCount = 0; this.nesting = []; + this.markers = new Set(); this.currentSection = null; this.currentNest = null; this.stageHandlers = { + 'mark': this.handleMark.bind(this), + 'async': this.handleAsync.bind(this), 'agent define': this.handleAgentDefine.bind(this), 'agent begin': this.handleAgentBegin.bind(this), 'agent end': this.handleAgentEnd.bind(this), @@ -83,6 +86,7 @@ define(['core/ArrayUtilities'], (array) => { agents: filteredAgents, mode, }); + this.currentNest.hasContent = true; } array.mergeSets(this.currentNest.agents, filteredAgents); array.mergeSets(this.agents, filteredAgents); @@ -100,6 +104,7 @@ define(['core/ArrayUtilities'], (array) => { }; this.currentNest = { agents, + hasContent: false, stage: { type: 'block', sections: [this.currentSection], @@ -114,6 +119,18 @@ define(['core/ArrayUtilities'], (array) => { 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}) { array.mergeSets(this.currentNest.agents, agents); array.mergeSets(this.agents, agents); @@ -149,10 +166,10 @@ define(['core/ArrayUtilities'], (array) => { if(this.nesting.length <= 1) { 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.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.agents, agents); this.addBounds( @@ -162,14 +179,18 @@ define(['core/ArrayUtilities'], (array) => { agents ); this.currentSection.stages.push(stage); + this.currentNest.hasContent = true; } } 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); - array.mergeSets(this.currentNest.agents, stage.agents); - array.mergeSets(this.agents, stage.agents); + this.currentNest.hasContent = true; } handleStage(stage) { @@ -183,6 +204,7 @@ define(['core/ArrayUtilities'], (array) => { generate({stages, meta = {}}) { this.agentStates.clear(); + this.markers.clear(); this.agents.length = 0; this.blockCount = 0; this.nesting.length = 0; diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index 99f7b4c..77bcc6f 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -31,6 +31,26 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { 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', () => { const sequence = generator.generate({stages: [ {type: '->', agents: ['A', 'B']}, @@ -369,10 +389,11 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { 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: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: AGENT_DEFINE, agents: ['A']}, + {type: 'mark', name: 'foo'}, {type: BLOCK_END}, ]}); diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index e6f5778..54a71aa 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -1,4 +1,4 @@ -define(() => { +define(['core/ArrayUtilities'], (array) => { 'use strict'; function execAt(str, reg, i) { @@ -160,124 +160,162 @@ define(() => { return list; } - function parseBlockCommand(line) { - 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(' '), - }; - } - - 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; + const PARSERS = [ + (line, meta) => { // title + if(line[0] !== 'title') { + return null; } - } - 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(' '); return true; - } - if(line[0] === 'terminators') { + }, + + (line, meta) => { // terminators + if(line[0] !== 'terminators') { + return null; + } + 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]; 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}) { - if(parseMeta(line, meta)) { - return; + let stage = null; + 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) { throw new Error('Unrecognised command: ' + line.join(' ')); } - stages.push(stage); + if(typeof stage === 'object') { + stages.push(stage); + } } return class Parser { diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index ad14f1b..7a5467a 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -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', () => { const parsed = parser.parse( 'if something happens\n' + diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 1db5a03..02b76c9 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -61,12 +61,14 @@ define([ }; this.separationAction = { + 'mark': this.separationMark.bind(this), + 'async': this.separationAsync.bind(this), 'agent begin': this.separationAgent.bind(this), 'agent end': this.separationAgent.bind(this), 'connection': this.separationConnection.bind(this), 'note over': this.separationNoteOver.bind(this), - 'note left': this.separationNoteLeft.bind(this), - 'note right': this.separationNoteRight.bind(this), + 'note left': this.separationNoteSide.bind(this, false), + 'note right': this.separationNoteSide.bind(this, true), 'note between': this.separationNoteBetween.bind(this), }; @@ -78,6 +80,8 @@ define([ }; this.renderAction = { + 'mark': this.renderMark.bind(this), + 'async': this.renderAsync.bind(this), 'agent begin': this.renderAgentBegin.bind(this), 'agent end': this.renderAgentEnd.bind(this), 'connection': this.renderConnection.bind(this), @@ -104,6 +108,7 @@ define([ this.width = 0; this.height = 0; + this.marks = new Map(); this.theme = theme; this.currentSequence = null; this.buildStaticElements(); @@ -180,6 +185,12 @@ define([ }); } + separationMark() { + } + + separationAsync() { + } + separationAgentCapBox({label}) { const config = this.theme.agentCap.box; const width = ( @@ -300,39 +311,23 @@ define([ this.addSeparations(this.visibleAgents, agentSpaces); } - separationNoteLeft({agents, mode, label}) { + separationNoteSide(isRight, {agents, mode, label}) { 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(); - agentSpaces.set(left, { - left: ( - this.sizer.measure(config.labelAttrs, label).width + - config.padding.left + - 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 - ), - }); + if(isRight) { + agentSpaces.set(right, {left: 0, right: width}); + } else { + agentSpaces.set(left, {left: width, right: 0}); + } this.addSeparations(this.visibleAgents, agentSpaces); } @@ -378,6 +373,18 @@ define([ 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}) { const config = this.theme.agentCap.box; 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}) { + this.checkAgentRange(agents); let maxHeight = 0; agents.forEach((agent) => { const agentInfo = this.agentInfos.get(agent); @@ -458,9 +488,11 @@ define([ agentInfo.latestYStart = this.currentY + shifts.lineBottom; }); this.currentY += maxHeight + this.theme.actionMargin; + this.markAgentRange(agents); } renderAgentEnd({mode, agents}) { + this.checkAgentRange(agents); let maxHeight = 0; agents.forEach((agent) => { const agentInfo = this.agentInfos.get(agent); @@ -477,6 +509,7 @@ define([ agentInfo.latestYStart = null; }); this.currentY += maxHeight + this.theme.actionMargin; + this.markAgentRange(agents); } renderSelfConnection({label, agents, line, left, right}) { @@ -609,11 +642,13 @@ define([ } renderConnection(stage) { + this.checkAgentRange(stage.agents); if(stage.agents[0] === stage.agents[1]) { this.renderSelfConnection(stage); } else { this.renderSimpleConnection(stage); } + this.markAgentRange(stage.agents); } renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) { @@ -679,6 +714,7 @@ define([ } renderNoteOver({agents, mode, label}) { + this.checkAgentRange(agents); const config = this.theme.note[mode]; if(agents.length > 1) { @@ -691,25 +727,31 @@ define([ const xMid = this.agentInfos.get(agents[0]).x; this.renderNote({xMid}, 'middle', mode, label); } + this.markAgentRange(agents); } renderNoteLeft({agents, mode, label}) { + this.checkAgentRange(agents); const config = this.theme.note[mode]; const {left} = this.findExtremes(agents); const x1 = this.agentInfos.get(left).x - config.margin.right; this.renderNote({x1}, 'end', mode, label); + this.markAgentRange(agents); } renderNoteRight({agents, mode, label}) { + this.checkAgentRange(agents); const config = this.theme.note[mode]; const {right} = this.findExtremes(agents); const x0 = this.agentInfos.get(right).x + config.margin.left; this.renderNote({x0}, 'start', mode, label); + this.markAgentRange(agents); } renderNoteBetween({agents, mode, label}) { + this.checkAgentRange(agents); const {left, right} = this.findExtremes(agents); const xMid = ( this.agentInfos.get(left).x + @@ -717,16 +759,20 @@ define([ ) / 2; 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; scope.y = this.currentY; scope.first = true; + this.markAgentRange([left, right]); } renderSectionBegin(scope, {left, right}, {mode, label}) { + this.checkAgentRange([left, right]); const config = this.theme.block; const agentInfoL = this.agentInfos.get(left); const agentInfoR = this.agentInfos.get(right); @@ -767,12 +813,14 @@ define([ Math.max(modeRender.height, labelRender.height) + config.section.padding.top ); + this.markAgentRange([left, right]); } renderSectionEnd(/*scope, block, section*/) { } renderBlockEnd(scope, {left, right}) { + this.checkAgentRange([left, right]); const config = this.theme.block; this.currentY += config.section.padding.bottom; @@ -786,6 +834,7 @@ define([ }, config.boxAttrs))); this.currentY += config.margin.bottom + this.theme.actionMargin; + this.markAgentRange([left, right]); } addAction(stage) { @@ -835,6 +884,7 @@ define([ index, x: null, latestYStart: null, + latestY: 0, separations: new Map(), }); }); @@ -884,6 +934,7 @@ define([ svg.empty(this.sections); svg.empty(this.actionShapes); svg.empty(this.actionLabels); + this.marks.clear(); this.title.set({ attrs: this.theme.titleAttrs, @@ -896,6 +947,7 @@ define([ this.currentY = 0; traverse(sequence.stages, this.renderTraversalFns); + this.checkAgentRange(['[', ']']); const stagesHeight = Math.max( this.currentY - this.theme.actionMargin,