diff --git a/README.md b/README.md index e7b6b57..6effe22 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Foo -> +Bar: Foo asks Bar [ <- Foo: To the left Foo -> ]: To the right Foo <- ]: From the right -[ -> ]: Left to right! +[ ~> ]: Wavy left to right! # (etc.) ``` diff --git a/screenshots/ConnectionTypes.png b/screenshots/ConnectionTypes.png index 1918926..613312c 100644 Binary files a/screenshots/ConnectionTypes.png and b/screenshots/ConnectionTypes.png differ diff --git a/scripts/core/ArrayUtilities.js b/scripts/core/ArrayUtilities.js index ad17e6e..f84e95a 100644 --- a/scripts/core/ArrayUtilities.js +++ b/scripts/core/ArrayUtilities.js @@ -56,24 +56,28 @@ define(() => { return list[list.length - 1]; } - function combineRecur(parts, position, str, target) { + function combineRecur(parts, position, current, target) { if(position >= parts.length) { - target.push(str); + target.push(current.slice()); return; } const choices = parts[position]; if(!Array.isArray(choices)) { - combineRecur(parts, position + 1, str + choices, target); + current.push(choices); + combineRecur(parts, position + 1, current, target); + current.pop(); return; } for(let i = 0; i < choices.length; ++ i) { - combineRecur(parts, position + 1, str + choices[i], target); + current.push(choices[i]); + combineRecur(parts, position + 1, current, target); + current.pop(); } } function combine(parts) { const target = []; - combineRecur(parts, 0, '', target); + combineRecur(parts, 0, [], target); return target; } diff --git a/scripts/core/ArrayUtilities_spec.js b/scripts/core/ArrayUtilities_spec.js index 7648652..c0fb9c9 100644 --- a/scripts/core/ArrayUtilities_spec.js +++ b/scripts/core/ArrayUtilities_spec.js @@ -179,10 +179,10 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => { ['Ff'], ]); expect(list).toEqual([ - 'AaCcEeFf', - 'AaDdEeFf', - 'BbCcEeFf', - 'BbDdEeFf', + ['Aa', 'Cc', 'Ee', 'Ff'], + ['Aa', 'Dd', 'Ee', 'Ff'], + ['Bb', 'Cc', 'Ee', 'Ff'], + ['Bb', 'Dd', 'Ee', 'Ff'], ]); }); }); diff --git a/scripts/main.js b/scripts/main.js index 1b9f45f..f5d703e 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -53,6 +53,10 @@ title: 'Open arrow', code: '{Agent1} ->> {Agent2}: {Message}', }, + { + title: 'Wavy line', + code: '{Agent1} ~> {Agent2}: {Message}', + }, { title: 'Self-connection', code: '{Agent1} -> {Agent1}: {Message}', diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index b4f21a2..7a16b40 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -3,17 +3,10 @@ define(['core/ArrayUtilities'], (array) => { const CM_ERROR = {type: 'error line-error', then: {'': 0}}; - const CM_COMMANDS = ((() => { + const makeCommands = ((() => { const end = {type: '', suggest: '\n', then: {}}; const hiddenEnd = {type: '', then: {}}; - const ARROWS = array.combine([ - ['', '<', '<<'], - ['-', '--'], - ['', '>', '>>'], - ]); - array.removeAll(ARROWS, ['-', '--']); - const textToEnd = {type: 'string', then: {'': 0, '\n': end}}; const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: { '': 0, @@ -108,7 +101,7 @@ define(['core/ArrayUtilities'], (array) => { }; } - function makeCMConnect() { + function makeCMConnect(arrows) { const connect = { type: 'keyword', suggest: true, @@ -124,11 +117,11 @@ define(['core/ArrayUtilities'], (array) => { }, '': 0, }; - ARROWS.forEach((arrow) => (then[arrow] = connect)); + arrows.forEach((arrow) => (then[arrow] = connect)); return makeOpBlock({type: 'variable', suggest: 'Agent', then}); } - return {type: 'error line-error', then: Object.assign({ + const BASE_THEN = { 'title': {type: 'keyword', suggest: true, then: { '': textToEnd, }}, @@ -220,7 +213,14 @@ define(['core/ArrayUtilities'], (array) => { }}, }}, }}, - }, makeCMConnect())}; + }; + + return (arrows) => { + return { + type: 'error line-error', + then: Object.assign(BASE_THEN, makeCMConnect(arrows)), + }; + }; })()); function cmCappedToken(token, current) { @@ -294,12 +294,12 @@ define(['core/ArrayUtilities'], (array) => { } } - function cmCheckToken(state, eol) { + function cmCheckToken(state, eol, commands) { const suggestions = { type: '', value: '', }; - let current = CM_COMMANDS; + let current = commands; const path = [current]; state.line.forEach((token, i) => { @@ -336,8 +336,9 @@ define(['core/ArrayUtilities'], (array) => { } return class Mode { - constructor(tokenDefinitions) { + constructor(tokenDefinitions, arrows) { this.tokenDefinitions = tokenDefinitions; + this.commands = makeCommands(arrows); this.lineComment = '#'; } @@ -348,7 +349,7 @@ define(['core/ArrayUtilities'], (array) => { currentQuoted: false, knownAgent: [], knownLabel: [], - beginCompletions: cmMakeCompletions({}, [CM_COMMANDS]), + beginCompletions: cmMakeCompletions({}, [this.commands]), completions: [], nextCompletions: [], valid: true, @@ -397,7 +398,7 @@ define(['core/ArrayUtilities'], (array) => { return 'comment'; } state.line.push({v: state.current, q: state.currentQuoted}); - return cmCheckToken(state, stream.eol()); + return cmCheckToken(state, stream.eol(), this.commands); } _tokenEOLFound(stream, state, block) { @@ -406,7 +407,7 @@ define(['core/ArrayUtilities'], (array) => { return 'comment'; } state.line.push(({v: state.current, q: state.currentQuoted})); - const type = cmCheckToken(state, false); + const type = cmCheckToken(state, false, this.commands); state.line.pop(); return type; } diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index 1f142f2..cb3422d 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -18,19 +18,32 @@ define([ }; const CONNECT_TYPES = ((() => { - const lTypes = ['', '<', '<<']; - const mTypes = ['-', '--']; - const rTypes = ['', '>', '>>']; - const arrows = array.combine([lTypes, mTypes, rTypes]); - array.removeAll(arrows, mTypes); + const lTypes = [ + {tok: '', type: 0}, + {tok: '<', type: 1}, + {tok: '<<', type: 2}, + ]; + const mTypes = [ + {tok: '-', type: 'solid'}, + {tok: '--', type: 'dash'}, + {tok: '~', type: 'wave'}, + ]; + const rTypes = [ + {tok: '', type: 0}, + {tok: '>', type: 1}, + {tok: '>>', type: 2}, + ]; + const arrows = (array.combine([lTypes, mTypes, rTypes]) + .filter((arrow) => (arrow[0].type !== 0 || arrow[2].type !== 0)) + ); 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)), + types.set(arrow.map((part) => part.tok).join(''), { + line: arrow[1].type, + left: arrow[0].type, + right: arrow[2].type, }); }); @@ -411,7 +424,9 @@ define([ return class Parser { getCodeMirrorMode() { - return SHARED_TOKENISER.getCodeMirrorMode(); + return SHARED_TOKENISER.getCodeMirrorMode( + Array.from(CONNECT_TYPES.keys()) + ); } getCodeMirrorHints() { diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 68f247a..9224627 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -195,22 +195,30 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { it('recognises all types of connection', () => { const parsed = parser.parse( - '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' + - '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' + + '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'], { @@ -219,21 +227,29 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { right: 1, label: '', }), + PARSED.connect(['A', 'B'], {line: 'solid', left: 0, right: 2}), 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: 'solid', left: 1, right: 2}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 0}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 1}), + PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 2}), PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 1}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 2}), 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: 0}), PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 1}), + PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 2}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 1}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 2}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 0}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 1}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 2}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 0}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 1}), + PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 2}), ]); }); diff --git a/scripts/sequence/Tokeniser.js b/scripts/sequence/Tokeniser.js index e9a9779..268f6cf 100644 --- a/scripts/sequence/Tokeniser.js +++ b/scripts/sequence/Tokeniser.js @@ -31,8 +31,8 @@ define(['./CodeMirrorMode'], (CMMode) => { unescape, baseToken: {q: true}, }, - {start: /(?=[^ \t\r\n:+\-*!<>,])/y, end: /(?=[ \t\r\n:+\-*!<>,])|$/y}, - {start: /(?=[\-<>])/y, end: /(?=[^\-<>])|$/y}, + {start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y}, + {start: /(?=[\-~<>])/y, end: /(?=[^\-~<>])|$/y}, {start: /,/y, baseToken: {v: ','}}, {start: /:/y, baseToken: {v: ':'}}, {start: /!/y, baseToken: {v: '!'}}, @@ -195,8 +195,8 @@ define(['./CodeMirrorMode'], (CMMode) => { return tokens; } - getCodeMirrorMode() { - return new CMMode(TOKENS); + getCodeMirrorMode(arrows) { + return new CMMode(TOKENS, arrows); } splitLines(tokens) { diff --git a/scripts/sequence/components/Connect.js b/scripts/sequence/components/Connect.js index dd45fd6..1805b28 100644 --- a/scripts/sequence/components/Connect.js +++ b/scripts/sequence/components/Connect.js @@ -90,6 +90,105 @@ define([ new Arrowhead('double'), ]; + function makeWavyLineHeights(height) { + return [ + 0, + -height * 2 / 3, + -height, + -height * 2 / 3, + 0, + height * 2 / 3, + height, + height * 2 / 3, + ]; + } + + class ConnectingLine { + renderFlat(container, {x1, x2, y}, attrs) { + const ww = attrs['wave-width']; + const hh = attrs['wave-height']; + + if(!ww || !hh) { + container.appendChild(svg.make('line', Object.assign({ + 'x1': x1, + 'y1': y, + 'x2': x2, + 'y2': y, + }, attrs))); + return; + } + + const heights = makeWavyLineHeights(hh); + const dw = ww / heights.length; + let p = 0; + + let points = ''; + for(let x = x1; x + dw <= x2; x += dw) { + points += ( + x + ' ' + + (y + heights[(p ++) % heights.length]) + ' ' + ); + } + points += x2 + ' ' + y; + container.appendChild(svg.make('polyline', Object.assign({ + points, + }, attrs))); + } + + renderRev(container, {xL1, xL2, y1, y2, xR}, attrs) { + const r = (y2 - y1) / 2; + const ww = attrs['wave-width']; + const hh = attrs['wave-height']; + + if(!ww || !hh) { + container.appendChild(svg.make('path', Object.assign({ + 'd': ( + 'M ' + xL1 + ' ' + y1 + + ' L ' + xR + ' ' + y1 + + ' A ' + r + ' ' + r + ' 0 0 1 ' + xR + ' ' + y2 + + ' L ' + xL2 + ' ' + y2 + ), + }, attrs))); + return; + } + + const heights = makeWavyLineHeights(hh); + const dw = ww / heights.length; + let p = 0; + + let points = ''; + for(let x = xL1; x + dw <= xR; x += dw) { + points += ( + x + ' ' + + (y1 + heights[(p ++) % heights.length]) + ' ' + ); + } + + const ym = (y1 + y2) / 2; + for(let t = 0; t + dw / r <= Math.PI; t += dw / r) { + const h = heights[(p ++) % heights.length]; + points += ( + (xR + Math.sin(t) * (r - h)) + ' ' + + (ym - Math.cos(t) * (r - h)) + ' ' + ); + } + + for(let x = xR; x - dw >= xL2; x -= dw) { + points += ( + x + ' ' + + (y2 - heights[(p ++) % heights.length]) + ' ' + ); + } + + points += xL2 + ' ' + y2; + container.appendChild(svg.make('polyline', Object.assign({ + points, + }, attrs))); + } + } + + const CONNECTING_LINE = new ConnectingLine(); + class Connect extends BaseComponent { separation({label, agentNames, options}, env) { const config = env.theme.connect; @@ -179,16 +278,13 @@ define([ const y1 = y0 + r * 2; const lineAttrs = config.lineAttrs[options.line]; - env.shapeLayer.appendChild(svg.make('path', Object.assign({ - 'd': ( - 'M ' + (lineX + lArrow.lineGap(env.theme, lineAttrs)) + - ' ' + y0 + - ' L ' + x1 + ' ' + y0 + - ' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 + - ' L ' + (lineX + rArrow.lineGap(env.theme, lineAttrs)) + - ' ' + y1 - ), - }, lineAttrs))); + CONNECTING_LINE.renderRev(env.shapeLayer, { + xL1: lineX + lArrow.lineGap(env.theme, lineAttrs), + xL2: lineX + rArrow.lineGap(env.theme, lineAttrs), + y1: y0, + y2: y1, + xR: x1, + }, lineAttrs); lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, dir: 1}); rArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y1, dir: 1}); @@ -241,12 +337,11 @@ define([ }); const lineAttrs = config.lineAttrs[options.line]; - env.shapeLayer.appendChild(svg.make('line', Object.assign({ - 'x1': x0 + lArrow.lineGap(env.theme, lineAttrs) * dir, - 'y1': y, - 'x2': x1 - rArrow.lineGap(env.theme, lineAttrs) * dir, - 'y2': y, - }, lineAttrs))); + CONNECTING_LINE.renderFlat(env.shapeLayer, { + x1: x0 + lArrow.lineGap(env.theme, lineAttrs) * dir, + x2: x1 - rArrow.lineGap(env.theme, lineAttrs) * dir, + y, + }, lineAttrs); lArrow.render(env.shapeLayer, env.theme, {x: x0, y, dir}); rArrow.render(env.shapeLayer, env.theme, {x: x1, y, dir: -dir}); diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js index d62ec07..79e5632 100644 --- a/scripts/sequence/themes/Basic.js +++ b/scripts/sequence/themes/Basic.js @@ -70,6 +70,15 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => { 'stroke-width': 1, 'stroke-dasharray': '4, 2', }, + 'wave': { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + 'wave-width': 6, + 'wave-height': 0.5, + }, }, arrow: { single: { diff --git a/scripts/sequence/themes/Chunky.js b/scripts/sequence/themes/Chunky.js index dc41f61..8ff48ac 100644 --- a/scripts/sequence/themes/Chunky.js +++ b/scripts/sequence/themes/Chunky.js @@ -76,6 +76,15 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => { 'stroke-width': 3, 'stroke-dasharray': '10, 4', }, + 'wave': { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + 'wave-width': 10, + 'wave-height': 1, + }, }, arrow: { single: {