diff --git a/README.md b/README.md index 27208ea..2f41469 100644 --- a/README.md +++ b/README.md @@ -40,25 +40,26 @@ terminators box ``` title Connection Types +begin Foo, Bar, Baz + Foo -> Bar: Simple arrow -Foo --> Bar: Dashed arrow +Bar --> Baz: Dashed arrow Foo <- Bar: Reversed arrow -Foo <-- Bar: Reversed dashed arrow +Bar <-- Baz: Reversed & dashed Foo <-> Bar: Double arrow -Foo <--> Bar: Double dashed arrow +Bar <--> Baz: Double dashed arrow # An arrow with no label: Foo -> Bar -Foo -> Foo: Foo talks to itself +Bar ->> Baz: Different arrow +Foo <<--> Bar: Mix of arrows + +Bar -> Bar: Bar talks to itself Foo -> +Bar: Foo asks Bar -Bar --> Foo: and Bar replies -# * and ! cause agents to be created and destroyed inline -Bar -> *Baz -Bar <- !Baz - # Arrows leaving on the left and right of the diagram [ -> Foo: From the left [ <- Foo: To the left @@ -139,12 +140,18 @@ too!' ``` title "Baz doesn't live long" -Foo -> Bar +note over Foo, Bar: Using begin / end + begin Baz Bar -> Baz Baz -> Foo end Baz -Foo -> Bar + +note over Foo, Bar: Using * / ! + +# * and ! cause agents to be created and destroyed inline +Bar -> *Baz: make Baz +Foo <- !Baz: end Baz # Foo and Bar end with black bars terminators bar diff --git a/screenshots/AgentAliases.png b/screenshots/AgentAliases.png index 4874eaa..5599d48 100644 Binary files a/screenshots/AgentAliases.png and b/screenshots/AgentAliases.png differ diff --git a/screenshots/ConnectionTypes.png b/screenshots/ConnectionTypes.png index 635302a..1918926 100644 Binary files a/screenshots/ConnectionTypes.png and b/screenshots/ConnectionTypes.png differ diff --git a/screenshots/Logic.png b/screenshots/Logic.png index ee38587..c68b853 100644 Binary files a/screenshots/Logic.png and b/screenshots/Logic.png differ diff --git a/screenshots/MultilineText.png b/screenshots/MultilineText.png index 668af85..d393a8b 100644 Binary files a/screenshots/MultilineText.png and b/screenshots/MultilineText.png differ diff --git a/screenshots/ShortLivedAgents.png b/screenshots/ShortLivedAgents.png index c02aa07..46f7204 100644 Binary files a/screenshots/ShortLivedAgents.png and b/screenshots/ShortLivedAgents.png differ diff --git a/screenshots/SimpleUsage.png b/screenshots/SimpleUsage.png index 8759381..cb2a986 100644 Binary files a/screenshots/SimpleUsage.png and b/screenshots/SimpleUsage.png differ diff --git a/scripts/core/ArrayUtilities.js b/scripts/core/ArrayUtilities.js index 7c2c5a1..ad17e6e 100644 --- a/scripts/core/ArrayUtilities.js +++ b/scripts/core/ArrayUtilities.js @@ -56,6 +56,27 @@ define(() => { return list[list.length - 1]; } + function combineRecur(parts, position, str, target) { + if(position >= parts.length) { + target.push(str); + return; + } + const choices = parts[position]; + if(!Array.isArray(choices)) { + combineRecur(parts, position + 1, str + choices, target); + return; + } + for(let i = 0; i < choices.length; ++ i) { + combineRecur(parts, position + 1, str + choices[i], target); + } + } + + function combine(parts) { + const target = []; + combineRecur(parts, 0, '', target); + return target; + } + return { indexOf, mergeSets, @@ -63,5 +84,6 @@ define(() => { removeAll, remove, last, + combine, }; }); diff --git a/scripts/core/ArrayUtilities_spec.js b/scripts/core/ArrayUtilities_spec.js index 931b904..7648652 100644 --- a/scripts/core/ArrayUtilities_spec.js +++ b/scripts/core/ArrayUtilities_spec.js @@ -169,4 +169,21 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => { expect(array.last([])).toEqual(undefined); }); }); + + describe('.combine', () => { + it('returns all combinations of the given arguments', () => { + const list = array.combine([ + ['Aa', 'Bb'], + ['Cc', 'Dd'], + 'Ee', + ['Ff'], + ]); + expect(list).toEqual([ + 'AaCcEeFf', + 'AaDdEeFf', + 'BbCcEeFf', + 'BbDdEeFf', + ]); + }); + }); }); diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index 4b11bab..f16f68e 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -7,11 +7,12 @@ define(['core/ArrayUtilities'], (array) => { const end = {type: '', suggest: '\n', then: {}}; const hiddenEnd = {type: '', then: {}}; - const ARROWS = [ - '->', '-->', - '<-', '<--', - '<->', '<-->', - ]; + const ARROWS = array.combine([ + ['', '<', '<<'], + ['-', '--'], + ['', '>', '>>'], + ]); + array.removeAll(ARROWS, ['-', '--']); const textToEnd = {type: 'string', then: {'': 0, '\n': end}}; const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: { diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index cae6a59..78ba5bd 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -52,8 +52,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { connect: (agentNames, { label = '', line = '', - left = false, - right = false, + left = 0, + right = 0, } = {}) => { return { type: 'connect', @@ -288,8 +288,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { PARSED.connect(['A', 'B'], { label: 'foo', line: 'bar', - left: true, - right: false, + left: 1, + right: 0, }), ]}); expect(sequence.stages).toEqual([ @@ -297,8 +297,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { GENERATED.connect(['A', 'B'], { label: 'foo', line: 'bar', - left: true, - right: false, + left: 1, + right: 0, }), jasmine.anything(), ]); diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index 539aaa3..fd66c10 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -12,18 +12,28 @@ define([ const BLOCK_TYPES = { 'if': {type: 'block begin', mode: 'if', skip: []}, 'else': {type: 'block split', mode: 'else', skip: ['if']}, - 'elif': {type: 'block split', mode: 'else', skip: []}, 'repeat': {type: 'block begin', mode: 'repeat', skip: []}, }; - const CONNECT_TYPES = { - '->': {line: 'solid', left: false, right: true}, - '<-': {line: 'solid', left: true, right: false}, - '<->': {line: 'solid', left: true, right: true}, - '-->': {line: 'dash', left: false, right: true}, - '<--': {line: 'dash', left: true, right: false}, - '<-->': {line: 'dash', left: true, right: true}, - }; + const CONNECT_TYPES = ((() => { + const lTypes = ['', '<', '<<']; + const mTypes = ['-', '--']; + const rTypes = ['', '>', '>>']; + const arrows = array.combine([lTypes, mTypes, rTypes]); + array.removeAll(arrows, mTypes); + + const types = new Map(); + + arrows.forEach((arrow) => { + types.set(arrow, { + line: arrow.includes('--') ? 'dash' : 'solid', + left: lTypes.indexOf(arrow.substr(0, arrow.indexOf('-'))), + right: rTypes.indexOf(arrow.substr(arrow.lastIndexOf('-') + 1)), + }); + }); + + return types; + })()); const CONNECT_AGENT_FLAGS = { '*': 'begin', @@ -323,7 +333,7 @@ define([ let typePos = -1; let options = null; for(let j = 0; j < line.length; ++ j) { - const opts = CONNECT_TYPES[tokenKeyword(line[j])]; + const opts = CONNECT_TYPES.get(tokenKeyword(line[j])); if(opts) { typePos = j; options = opts; diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 8f2cdf7..b0e7c99 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -200,45 +200,40 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { 'A <-> B\n' + 'A --> B\n' + 'A <-- B\n' + - 'A <--> B\n' + 'A <--> B\n' + + 'A ->> B\n' + + 'A <<- B\n' + + 'A <<->> B\n' + + 'A <->> B\n' + + 'A <<-> B\n' + + 'A -->> B\n' + + 'A <<-- B\n' + + 'A <<-->> B\n' + + 'A <-->> B\n' + + 'A <<--> B\n' ); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B'], { line: 'solid', - left: false, - right: true, - label: '', - }), - PARSED.connect(['A', 'B'], { - line: 'solid', - left: true, - right: false, - label: '', - }), - PARSED.connect(['A', 'B'], { - line: 'solid', - left: true, - right: true, - label: '', - }), - PARSED.connect(['A', 'B'], { - line: 'dash', - left: false, - right: true, - label: '', - }), - PARSED.connect(['A', 'B'], { - line: 'dash', - left: true, - right: false, - label: '', - }), - PARSED.connect(['A', 'B'], { - line: 'dash', - left: true, - right: true, + left: 0, + right: 1, label: '', }), + PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 0}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 1}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 1}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 0}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 1}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 0, right: 2}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 0}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 2}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 2}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 1}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 2}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 0}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 2}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 2}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 1}), ]); }); @@ -250,14 +245,14 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B'], { line: 'solid', - left: true, - right: false, + left: 1, + right: 0, label: 'B -> A', }), PARSED.connect(['A', 'B'], { line: 'solid', - left: false, - right: true, + left: 0, + right: 1, label: 'B <- A', }), ]); diff --git a/scripts/sequence/Renderer_spec.js b/scripts/sequence/Renderer_spec.js index 7b8ff0e..f1d283c 100644 --- a/scripts/sequence/Renderer_spec.js +++ b/scripts/sequence/Renderer_spec.js @@ -33,8 +33,8 @@ defineDescribe('Sequence Renderer', [ label, options: { line: 'solid', - left: false, - right: true, + left: 0, + right: 1, }, }; }, diff --git a/scripts/sequence/components/Connect.js b/scripts/sequence/components/Connect.js index 340e5eb..7c91930 100644 --- a/scripts/sequence/components/Connect.js +++ b/scripts/sequence/components/Connect.js @@ -22,31 +22,87 @@ define([ )); } - function getArrowShort(theme) { - const arrow = theme.connect.arrow; - const join = arrow.attrs['stroke-linejoin'] || 'miter'; - const t = arrow.attrs['stroke-width'] * 0.5; - const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5; - if(join === 'round') { - return lineStroke + t; - } else { - const h = arrow.height / 2; - const w = arrow.width; - const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1); - return lineStroke + arrowDistance; + class Arrowhead { + constructor(propName) { + this.propName = propName; + } + + getConfig(theme) { + return theme.connect.arrow[this.propName]; + } + + short(theme) { + const arrow = this.getConfig(theme); + const join = arrow.attrs['stroke-linejoin'] || 'miter'; + const t = arrow.attrs['stroke-width'] * 0.5; + const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5; + if(join === 'round') { + return lineStroke + t; + } else { + const h = arrow.height / 2; + const w = arrow.width; + const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1); + return lineStroke + arrowDistance; + } + } + + render(layer, theme, {x, y, dir}) { + const config = this.getConfig(theme); + drawHorizontalArrowHead(layer, { + x: x + this.short(theme) * dir, + y, + dx: config.width * dir, + dy: config.height / 2, + attrs: config.attrs, + }); + } + + width(theme) { + return this.short(theme) + this.getConfig(theme).width; + } + + height(theme) { + return this.getConfig(theme).height; + } + + lineGap(theme, lineAttrs) { + const arrow = this.getConfig(theme); + const short = this.short(theme); + if(arrow.attrs.fill === 'none') { + const h = arrow.height / 2; + const w = arrow.width; + const safe = short + (lineAttrs['stroke-width'] / 2) * (w / h); + return (short + safe) / 2; + } else { + return short + arrow.width / 2; + } } } + const ARROWHEADS = [ + { + render: () => {}, + width: () => 0, + height: () => 0, + lineGap: () => 0, + }, + new Arrowhead('single'), + new Arrowhead('double'), + ]; + class Connect extends BaseComponent { - separation({agentNames, label}, env) { + separation({label, agentNames, options}, env) { const config = env.theme.connect; - const labelWidth = ( - env.textSizer.measure(config.label.attrs, label).width + - config.label.padding * 2 - ); + const lArrow = ARROWHEADS[options.left]; + const rArrow = ARROWHEADS[options.right]; - const short = getArrowShort(env.theme); + let labelWidth = ( + env.textSizer.measure(config.label.attrs, label).width + ); + if(labelWidth > 0) { + labelWidth += config.label.padding * 2; + } const info1 = env.agentInfos.get(agentNames[0]); if(agentNames[0] === agentNames[1]) { @@ -54,9 +110,10 @@ define([ left: 0, right: ( info1.currentMaxRad + - labelWidth + - config.arrow.width + - short + + Math.max( + labelWidth + lArrow.width(env.theme), + rArrow.width(env.theme) + ) + config.loopbackRadius ), }); @@ -69,8 +126,10 @@ define([ info1.currentMaxRad + info2.currentMaxRad + labelWidth + - config.arrow.width * 2 + - short * 2 + Math.max( + lArrow.width(env.theme), + rArrow.width(env.theme) + ) * 2 ); } } @@ -79,9 +138,8 @@ define([ const config = env.theme.connect; const from = env.agentInfos.get(agentNames[0]); - const dx = config.arrow.width; - const dy = config.arrow.height / 2; - const short = getArrowShort(env.theme); + const lArrow = ARROWHEADS[options.left]; + const rArrow = ARROWHEADS[options.right]; const height = ( env.textSizer.measureHeight(config.label.attrs, label) + @@ -93,9 +151,8 @@ define([ const y0 = env.primaryY; const x0 = ( lineX + - short + - dx + - config.label.padding + lArrow.width(env.theme) + + (label ? config.label.padding : 0) ); const renderedText = SVGShapes.renderBoxedText(label, { @@ -108,48 +165,32 @@ define([ labelLayer: env.labelLayer, SVGTextBlockClass: env.SVGTextBlockClass, }); - const r = config.loopbackRadius; - const x1 = ( - x0 + + const labelW = (label ? ( renderedText.width + config.label.padding - 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 space = short + dx / 2; - + const lineAttrs = config.lineAttrs[options.line]; env.shapeLayer.appendChild(svg.make('path', Object.assign({ 'd': ( - 'M ' + (lineX + (options.left ? space : 0)) + ' ' + y0 + + 'M ' + (lineX + lArrow.lineGap(env.theme, lineAttrs)) + + ' ' + y0 + ' L ' + x1 + ' ' + y0 + ' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 + - ' L ' + (lineX + (options.right ? space : 0)) + ' ' + y1 + ' L ' + (lineX + rArrow.lineGap(env.theme, lineAttrs)) + + ' ' + y1 ), - }, config.lineAttrs[options.line]))); + }, lineAttrs))); - if(options.left) { - drawHorizontalArrowHead(env.shapeLayer, { - x: lineX + short, - y: y0, - dx, - dy, - attrs: config.arrow.attrs, - }); - } + lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, dir: 1}); + rArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y1, dir: 1}); - if(options.right) { - drawHorizontalArrowHead(env.shapeLayer, { - x: lineX + short, - y: y1, - dx, - dy, - attrs: config.arrow.attrs, - }); - } - - return y1 + dy + env.theme.actionMargin; + return y1 + rArrow.height(env.theme) / 2 + env.theme.actionMargin; } renderSimpleConnect({label, agentNames, options}, env) { @@ -157,10 +198,10 @@ define([ const from = env.agentInfos.get(agentNames[0]); const to = env.agentInfos.get(agentNames[1]); - const dx = config.arrow.width; - const dy = config.arrow.height / 2; + const lArrow = ARROWHEADS[options.left]; + const rArrow = ARROWHEADS[options.right]; + const dir = (from.x < to.x) ? 1 : -1; - const short = getArrowShort(env.theme); const height = ( env.textSizer.measureHeight(config.label.attrs, label) + @@ -183,50 +224,47 @@ define([ SVGTextBlockClass: env.SVGTextBlockClass, }); - const space = short + dx / 2; - + const lineAttrs = config.lineAttrs[options.line]; env.shapeLayer.appendChild(svg.make('line', Object.assign({ - 'x1': x0 + (options.left ? space : 0) * dir, + 'x1': x0 + lArrow.lineGap(env.theme, lineAttrs) * dir, 'y1': y, - 'x2': x1 - (options.right ? space : 0) * dir, + 'x2': x1 - rArrow.lineGap(env.theme, lineAttrs) * dir, 'y2': y, - }, config.lineAttrs[options.line]))); + }, lineAttrs))); - if(options.left) { - drawHorizontalArrowHead(env.shapeLayer, { - x: x0 + short * dir, - y, - dx: dx * dir, - dy, - attrs: config.arrow.attrs, - }); - } + lArrow.render(env.shapeLayer, env.theme, {x: x0, y, dir}); + rArrow.render(env.shapeLayer, env.theme, {x: x1, y, dir: -dir}); - if(options.right) { - drawHorizontalArrowHead(env.shapeLayer, { - x: x1 - short * dir, - y, - dx: -dx * dir, - dy, - attrs: config.arrow.attrs, - }); - } - - return y + dy + env.theme.actionMargin; + return ( + y + + Math.max( + lArrow.height(env.theme), + rArrow.height(env.theme) + ) / 2 + + env.theme.actionMargin + ); } - renderPre({label, agentNames}, env) { + renderPre({label, agentNames, options}, env) { const config = env.theme.connect; + const lArrow = ARROWHEADS[options.left]; + const rArrow = ARROWHEADS[options.right]; + const height = ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ); + let arrowH = lArrow.height(env.theme); + if(agentNames[0] !== agentNames[1]) { + arrowH = Math.max(arrowH, rArrow.height(env.theme)); + } + return { agentNames, - topShift: Math.max(config.arrow.height / 2, height), + topShift: Math.max(arrowH / 2, height), }; } diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js index 1324679..d62ec07 100644 --- a/scripts/sequence/themes/Basic.js +++ b/scripts/sequence/themes/Basic.js @@ -72,12 +72,24 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => { }, }, arrow: { - width: 5, - height: 10, - attrs: { - 'fill': '#000000', - 'stroke-width': 0, - 'stroke-linejoin': 'miter', + single: { + width: 5, + height: 10, + attrs: { + 'fill': '#000000', + 'stroke-width': 0, + 'stroke-linejoin': 'miter', + }, + }, + double: { + width: 4, + height: 6, + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-linejoin': 'miter', + }, }, }, label: { diff --git a/scripts/sequence/themes/Chunky.js b/scripts/sequence/themes/Chunky.js index 80a39a0..dc41f61 100644 --- a/scripts/sequence/themes/Chunky.js +++ b/scripts/sequence/themes/Chunky.js @@ -78,17 +78,30 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => { }, }, arrow: { - width: 10, - height: 12, - attrs: { - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-linejoin': 'round', + single: { + width: 10, + height: 12, + attrs: { + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + }, + }, + double: { + width: 10, + height: 12, + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + }, }, }, label: { - padding: 6, + padding: 7, margin: {top: 2, bottom: 3}, attrs: { 'font-family': 'sans-serif',