Add found messages and fix minor rendering issue with groups containing arrows to sides [#37]

This commit is contained in:
David Evans 2018-01-16 22:32:26 +00:00
parent b8491e25dc
commit 394dcb0e42
15 changed files with 853 additions and 205 deletions

View File

@ -645,8 +645,13 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
)}; )};
} }
const colonTextToEnd = {
type: 'operator',
suggest: true,
then: {'': textToEnd, '\n': hiddenEnd},
};
const agentListToText = agentListTo({ const agentListToText = agentListTo({
':': {type: 'operator', suggest: true, then: {'': textToEnd}}, ':': colonTextToEnd,
}); });
const agentList2ToText = {type: 'variable', suggest: 'Agent', then: { const agentList2ToText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
@ -656,7 +661,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
const singleAgentToText = {type: 'variable', suggest: 'Agent', then: { const singleAgentToText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
',': CM_ERROR, ',': CM_ERROR,
':': {type: 'operator', suggest: true, then: {'': textToEnd}}, ':': colonTextToEnd,
}}; }};
const agentToOptText = {type: 'variable', suggest: 'Agent', then: { const agentToOptText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
@ -700,7 +705,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}; };
} }
function makeOpBlock(exit) { function makeOpBlock(exit, sourceExit) {
const op = {type: 'operator', suggest: true, then: { const op = {type: 'operator', suggest: true, then: {
'+': CM_ERROR, '+': CM_ERROR,
'-': CM_ERROR, '-': CM_ERROR,
@ -729,13 +734,13 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}}, }},
'': exit, '': exit,
}}, }},
'*': {type: 'operator', suggest: true, then: { '*': {type: 'operator', suggest: true, then: Object.assign({
'+': op, '+': op,
'-': op, '-': op,
'*': CM_ERROR, '*': CM_ERROR,
'!': CM_ERROR, '!': CM_ERROR,
'': exit, '': exit,
}}, }, sourceExit)},
'!': op, '!': op,
'': exit, '': exit,
}; };
@ -745,7 +750,10 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
const connect = { const connect = {
type: 'keyword', type: 'keyword',
suggest: true, suggest: true,
then: makeOpBlock(agentToOptText), then: makeOpBlock(agentToOptText, {
':': colonTextToEnd,
'\n': hiddenEnd,
}),
}; };
const then = {'': 0}; const then = {'': 0};
@ -756,7 +764,10 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
override: 'Label', override: 'Label',
then: {}, then: {},
}; };
return makeOpBlock({type: 'variable', suggest: 'Agent', then}); return makeOpBlock(
{type: 'variable', suggest: 'Agent', then},
then
);
} }
const BASE_THEN = { const BASE_THEN = {
@ -1597,10 +1608,10 @@ define('sequence/Parser',[
})()); })());
const CONNECT_AGENT_FLAGS = { const CONNECT_AGENT_FLAGS = {
'*': 'begin', '*': {flag: 'begin', allowBlankName: true, blankNameFlag: 'source'},
'+': 'start', '+': {flag: 'start'},
'-': 'stop', '-': {flag: 'stop'},
'!': 'end', '!': {flag: 'end'},
}; };
const TERMINATOR_TYPES = [ const TERMINATOR_TYPES = [
@ -1712,7 +1723,7 @@ define('sequence/Parser',[
return -1; return -1;
} }
function readAgentAlias(line, start, end, enableAlias) { function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
let aliasSep = -1; let aliasSep = -1;
if(enableAlias) { if(enableAlias) {
aliasSep = findToken(line, 'as', start); aliasSep = findToken(line, 'as', start);
@ -1720,7 +1731,7 @@ define('sequence/Parser',[
if(aliasSep === -1 || aliasSep >= end) { if(aliasSep === -1 || aliasSep >= end) {
aliasSep = end; aliasSep = end;
} }
if(start >= aliasSep) { if(start >= aliasSep && !allowBlankName) {
throw makeError('Missing agent name', errToken(line, start)); throw makeError('Missing agent name', errToken(line, start));
} }
return { return {
@ -1734,25 +1745,31 @@ define('sequence/Parser',[
aliases = false, aliases = false,
} = {}) { } = {}) {
const flags = []; const flags = [];
const blankNameFlags = [];
let p = start; let p = start;
let allowBlankName = false;
for(; p < end; ++ p) { for(; p < end; ++ p) {
const token = line[p]; const token = line[p];
const rawFlag = tokenKeyword(token); const rawFlag = tokenKeyword(token);
const flag = flagTypes[rawFlag]; const flag = flagTypes[rawFlag];
if(flag) { if(!flag) {
if(flags.includes(flag)) {
throw makeError('Duplicate agent flag: ' + rawFlag, token);
}
flags.push(flag);
} else {
break; break;
} }
if(flags.includes(flag.flag)) {
throw makeError('Duplicate agent flag: ' + rawFlag, token);
} }
const {name, alias} = readAgentAlias(line, p, end, aliases); allowBlankName = allowBlankName || Boolean(flag.allowBlankName);
flags.push(flag.flag);
blankNameFlags.push(flag.blankNameFlag);
}
const {name, alias} = readAgentAlias(line, p, end, {
enableAlias: aliases,
allowBlankName,
});
return { return {
name, name,
alias, alias,
flags, flags: name ? flags : blankNameFlags,
}; };
} }
@ -2091,8 +2108,8 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
equals: (a, b) => { equals: (a, b) => {
return a.id === b.id; return a.id === b.id;
}, },
make: (id, {anchorRight = false} = {}) => { make: (id, {anchorRight = false, isVirtualSource = false} = {}) => {
return {id, anchorRight}; return {id, anchorRight, isVirtualSource};
}, },
indexOf: (list, gAgent) => { indexOf: (list, gAgent) => {
return array.indexOf(list, gAgent, GAgent.equals); return array.indexOf(list, gAgent, GAgent.equals);
@ -2100,6 +2117,14 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
hasIntersection: (a, b) => { hasIntersection: (a, b) => {
return array.hasIntersection(a, b, GAgent.equals); return array.hasIntersection(a, b, GAgent.equals);
}, },
addNearby: (target, reference, item, offset) => {
const p = array.indexOf(target, reference, GAgent.equals);
if(p === -1) {
target.push(item);
} else {
target.splice(p + offset, 0, item);
}
},
}; };
const NOTE_DEFAULT_G_AGENTS = { const NOTE_DEFAULT_G_AGENTS = {
@ -2108,6 +2133,8 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
'note right': [GAgent.make(']')], 'note right': [GAgent.make(']')],
}; };
const SPECIAL_AGENT_IDS = ['[', ']'];
const MERGABLE = { const MERGABLE = {
'agent begin': { 'agent begin': {
check: ['mode'], check: ['mode'],
@ -2282,7 +2309,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
this.activeGroups = new Map(); this.activeGroups = new Map();
this.gAgents = []; this.gAgents = [];
this.labelPattern = null; this.labelPattern = null;
this.blockCount = 0; this.nextID = 0;
this.nesting = []; this.nesting = [];
this.markers = new Set(); this.markers = new Set();
this.currentSection = null; this.currentSection = null;
@ -2311,7 +2338,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
this.endGroup = this.endGroup.bind(this); this.endGroup = this.endGroup.bind(this);
} }
toGAgent({alias, name}) { toGAgent({name, alias, flags}) {
if(alias) { if(alias) {
if(this.agentAliases.has(name)) { if(this.agentAliases.has(name)) {
throw new Error( throw new Error(
@ -2330,7 +2357,9 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
} }
this.agentAliases.set(alias, name); this.agentAliases.set(alias, name);
} }
return GAgent.make(this.agentAliases.get(name) || name); return GAgent.make(this.agentAliases.get(name) || name, {
isVirtualSource: flags.includes('source'),
});
} }
addStage(stage, isVisible = true) { addStage(stage, isVisible = true) {
@ -2366,7 +2395,12 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
} }
defineGAgents(gAgents) { defineGAgents(gAgents) {
array.mergeSets(this.currentNest.gAgents, gAgents, GAgent.equals); array.mergeSets(
this.currentNest.gAgents,
gAgents.filter((gAgent) =>
!SPECIAL_AGENT_IDS.includes(gAgent.id)),
GAgent.equals
);
array.mergeSets(this.gAgents, gAgents, GAgent.equals); array.mergeSets(this.gAgents, gAgents, GAgent.equals);
} }
@ -2390,7 +2424,9 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
validateGAgents(gAgents, { validateGAgents(gAgents, {
allowGrouped = false, allowGrouped = false,
rejectGrouped = false, rejectGrouped = false,
allowVirtual = false,
} = {}) { } = {}) {
/* jshint -W074 */ // agent validity checking requires several steps
gAgents.forEach((gAgent) => { gAgents.forEach((gAgent) => {
const state = this.getGAgentState(gAgent); const state = this.getGAgentState(gAgent);
if(state.covered) { if(state.covered) {
@ -2404,6 +2440,9 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
if(state.blocked && (!allowGrouped || state.group === null)) { if(state.blocked && (!allowGrouped || state.group === null)) {
throw new Error('Duplicate agent name: ' + gAgent.id); throw new Error('Duplicate agent name: ' + gAgent.id);
} }
if(!allowVirtual && gAgent.isVirtualSource) {
throw new Error('cannot use message source here');
}
if(gAgent.id.startsWith('__')) { if(gAgent.id.startsWith('__')) {
throw new Error(gAgent.id + ' is a reserved name'); throw new Error(gAgent.id + ' is a reserved name');
} }
@ -2497,12 +2536,18 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
this.replaceGAgentState(rightGAgent, AgentState.LOCKED); this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
this.nesting.push(this.currentNest); this.nesting.push(this.currentNest);
return {gAgents, stages}; return {stages};
} }
nextBlockName() { nextBlockName() {
const name = '__BLOCK' + this.blockCount; const name = '__BLOCK' + this.nextID;
++ this.blockCount; ++ this.nextID;
return name;
}
nextVirtualAgentName() {
const name = '__' + this.nextID;
++ this.nextID;
return name; return name;
} }
@ -2706,9 +2751,13 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]); let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]);
let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]); let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]);
if(ind1 === -1) { if(ind1 === -1) {
ind1 = this.gAgents.length; // Virtual sources written as '* -> Ref' will spawn to the left,
// not the right (as non-virtual agents would)
ind1 = gAgents1[0].isVirtualSource ? -1 : this.gAgents.length;
} }
if(ind2 === -1) { if(ind2 === -1) {
// Virtual and non-virtual agents written as 'Ref -> *' will
// spawn to the right
ind2 = this.gAgents.length; ind2 = this.gAgents.length;
} }
if(ind1 === ind2) { if(ind1 === ind2) {
@ -2755,21 +2804,79 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
return {beginGAgents, endGAgents, startGAgents, stopGAgents}; return {beginGAgents, endGAgents, startGAgents, stopGAgents};
} }
makeVirtualAgent(anchorRight) {
const virtualGAgent = GAgent.make(this.nextVirtualAgentName(), {
anchorRight,
isVirtualSource: true,
});
this.replaceGAgentState(virtualGAgent, AgentState.LOCKED);
return virtualGAgent;
}
addNearbyAgent(gAgentReference, gAgent, offset) {
GAgent.addNearby(
this.currentNest.gAgents,
gAgentReference,
gAgent,
offset
);
GAgent.addNearby(
this.gAgents,
gAgentReference,
gAgent,
offset
);
}
expandVirtualSourceAgents(gAgents) {
if(gAgents[0].isVirtualSource) {
if(gAgents[1].isVirtualSource) {
throw new Error('Cannot connect found messages');
}
if(SPECIAL_AGENT_IDS.includes(gAgents[1].id)) {
throw new Error(
'Cannot connect found messages to special agents'
);
}
const virtualGAgent = this.makeVirtualAgent(true);
this.addNearbyAgent(gAgents[1], virtualGAgent, 0);
return [virtualGAgent, gAgents[1]];
}
if(gAgents[1].isVirtualSource) {
if(SPECIAL_AGENT_IDS.includes(gAgents[0].id)) {
throw new Error(
'Cannot connect found messages to special agents'
);
}
const virtualGAgent = this.makeVirtualAgent(false);
this.addNearbyAgent(gAgents[0], virtualGAgent, 1);
return [gAgents[0], virtualGAgent];
}
return gAgents;
}
handleConnect({agents, label, options}) { handleConnect({agents, label, options}) {
const flags = this.filterConnectFlags(agents); const flags = this.filterConnectFlags(agents);
let gAgents = agents.map(this.toGAgent); let gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents, {allowGrouped: true}); this.validateGAgents(gAgents, {
allowGrouped: true,
allowVirtual: true,
});
const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent); const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
this.defineGAgents(allGAgents); this.defineGAgents(allGAgents
.filter((gAgent) => !gAgent.isVirtualSource)
);
gAgents = this.expandGroupedGAgentConnection(gAgents); gAgents = this.expandGroupedGAgentConnection(gAgents);
gAgents = this.expandVirtualSourceAgents(gAgents);
const agentIDs = gAgents.map((gAgent) => gAgent.id); const agentIDs = gAgents.map((gAgent) => gAgent.id);
const implicitBeginGAgents = (agents const implicitBeginGAgents = (agents
.filter(PAgent.hasFlag('begin', false)) .filter(PAgent.hasFlag('begin', false))
.map(this.toGAgent) .map(this.toGAgent)
.filter((gAgent) => !gAgent.isVirtualSource)
); );
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
@ -2866,7 +2973,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
this.agentAliases.clear(); this.agentAliases.clear();
this.activeGroups.clear(); this.activeGroups.clear();
this.gAgents.length = 0; this.gAgents.length = 0;
this.blockCount = 0; this.nextID = 0;
this.nesting.length = 0; this.nesting.length = 0;
this.labelPattern = [{token: 'label'}]; this.labelPattern = [{token: 'label'}];
} }
@ -3457,6 +3564,7 @@ define('sequence/components/BaseComponent',[],() => {
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
momentaryAgentIDs,
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
@ -3469,6 +3577,7 @@ define('sequence/components/BaseComponent',[],() => {
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
momentaryAgentIDs,
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
@ -4211,10 +4320,12 @@ define('sequence/components/AgentHighlight',['./BaseComponent'], (BaseComponent)
}); });
define('sequence/components/Connect',[ define('sequence/components/Connect',[
'core/ArrayUtilities',
'./BaseComponent', './BaseComponent',
'svg/SVGUtilities', 'svg/SVGUtilities',
'svg/SVGShapes', 'svg/SVGShapes',
], ( ], (
array,
BaseComponent, BaseComponent,
svg, svg,
SVGShapes SVGShapes
@ -4318,6 +4429,18 @@ define('sequence/components/Connect',[
]; ];
class Connect extends BaseComponent { class Connect extends BaseComponent {
separationPre({agentIDs}, env) {
const r = env.theme.connect.source.radius;
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
if(!agentInfo.isVirtualSource) {
return;
}
agentInfo.currentRad = r;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
}
separation({label, agentIDs, options}, env) { separation({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
@ -4359,6 +4482,8 @@ define('sequence/components/Connect',[
) * 2 ) * 2
); );
} }
array.mergeSets(env.momentaryAgentIDs, agentIDs);
} }
renderSelfConnect({label, agentIDs, options}, env) { renderSelfConnect({label, agentIDs, options}, env) {
@ -4436,14 +4561,59 @@ define('sequence/components/Connect',[
); );
} }
renderSimpleLine(x0, x1, options, env) {
const dir = (x0 < x1) ? 1 : -1;
const config = env.theme.connect;
const line = config.line[options.line];
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
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,
});
env.shapeLayer.appendChild(rendered.shape);
return rendered;
}
renderSimpleArrowheads(options, renderedLine, env, dir) {
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
lArrow.render(env.shapeLayer, env.theme, renderedLine.p1, dir);
rArrow.render(env.shapeLayer, env.theme, renderedLine.p2, -dir);
return {lArrow, rArrow};
}
renderVirtualSources(from, to, renderedLine, env) {
const config = env.theme.connect.source;
if(from.isVirtualSource) {
env.shapeLayer.appendChild(config.render({
x: renderedLine.p1.x - config.radius,
y: renderedLine.p1.y,
radius: config.radius,
}));
}
if(to.isVirtualSource) {
env.shapeLayer.appendChild(config.render({
x: renderedLine.p2.x + config.radius,
y: renderedLine.p2.y,
radius: config.radius,
}));
}
}
renderSimpleConnect({label, agentIDs, options}, env) { renderSimpleConnect({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
const from = env.agentInfos.get(agentIDs[0]); const from = env.agentInfos.get(agentIDs[0]);
const to = env.agentInfos.get(agentIDs[1]); const to = env.agentInfos.get(agentIDs[1]);
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const dir = (from.x < to.x) ? 1 : -1; const dir = (from.x < to.x) ? 1 : -1;
const height = ( const height = (
@ -4469,18 +4639,12 @@ define('sequence/components/Connect',[
SVGTextBlockClass: env.SVGTextBlockClass, SVGTextBlockClass: env.SVGTextBlockClass,
}); });
const line = config.line[options.line]; const rendered = this.renderSimpleLine(x0, x1, options, env);
const rendered = line.renderFlat(line.attrs, { const {
x1: x0, lArrow,
dx1: lArrow.lineGap(env.theme, line.attrs) * dir, rArrow
x2: x1, } = this.renderSimpleArrowheads(options, rendered, env, dir);
dx2: -rArrow.lineGap(env.theme, line.attrs) * dir, this.renderVirtualSources(from, to, rendered, env);
y,
});
env.shapeLayer.appendChild(rendered.shape);
lArrow.render(env.shapeLayer, env.theme, rendered.p1, dir);
rArrow.render(env.shapeLayer, env.theme, rendered.p2, -dir);
const arrowSpread = Math.max( const arrowSpread = Math.max(
lArrow.height(env.theme), lArrow.height(env.theme),
@ -4996,6 +5160,7 @@ define('sequence/Renderer',[
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
visibleAgentIDs: this.visibleAgentIDs, visibleAgentIDs: this.visibleAgentIDs,
momentaryAgentIDs: agentIDs,
textSizer: this.sizer, textSizer: this.sizer,
addSpacing, addSpacing,
addSeparation: this.addSeparation, addSeparation: this.addSeparation,
@ -5200,6 +5365,7 @@ define('sequence/Renderer',[
id: agent.id, id: agent.id,
formattedLabel: agent.formattedLabel, formattedLabel: agent.formattedLabel,
anchorRight: agent.anchorRight, anchorRight: agent.anchorRight,
isVirtualSource: agent.isVirtualSource,
index, index,
x: null, x: null,
latestYStart: null, latestYStart: null,
@ -5906,6 +6072,19 @@ define('sequence/themes/Basic',[
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 2,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
});
},
},
mask: { mask: {
padding: { padding: {
top: 0, top: 0,
@ -6227,6 +6406,19 @@ define('sequence/themes/Monospace',[
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 2,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
});
},
},
mask: { mask: {
padding: { padding: {
top: 0, top: 0,
@ -6547,6 +6739,19 @@ define('sequence/themes/Chunky',[
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 5,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 3,
});
},
},
mask: { mask: {
padding: { padding: {
top: 1, top: 1,
@ -7287,6 +7492,19 @@ define('sequence/themes/Sketch',[
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 1,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
});
},
},
mask: { mask: {
padding: { padding: {
top: 0, top: 0,
@ -7722,7 +7940,7 @@ define('sequence/themes/Sketch',[
); );
return { return {
shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)), shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)),
p1: {x: ln.p1.x - dx1, y: ln.p2.y}, p1: {x: ln.p1.x - dx1, y: ln.p1.y},
p2: {x: ln.p2.x - dx2, y: ln.p2.y}, p2: {x: ln.p2.x - dx2, y: ln.p2.y},
}; };
} }

File diff suppressed because one or more lines are too long

View File

@ -56,6 +56,10 @@
title: 'Self-connection', title: 'Self-connection',
code: '{Agent1} -> {Agent1}: {Message}', code: '{Agent1} -> {Agent1}: {Message}',
}, },
{
title: 'Found message',
code: '* -> {Agent1}: {Message}',
},
{ {
title: 'Request/response pair', title: 'Request/response pair',
code: ( code: (

View File

@ -42,8 +42,13 @@ define(['core/ArrayUtilities'], (array) => {
)}; )};
} }
const colonTextToEnd = {
type: 'operator',
suggest: true,
then: {'': textToEnd, '\n': hiddenEnd},
};
const agentListToText = agentListTo({ const agentListToText = agentListTo({
':': {type: 'operator', suggest: true, then: {'': textToEnd}}, ':': colonTextToEnd,
}); });
const agentList2ToText = {type: 'variable', suggest: 'Agent', then: { const agentList2ToText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
@ -53,7 +58,7 @@ define(['core/ArrayUtilities'], (array) => {
const singleAgentToText = {type: 'variable', suggest: 'Agent', then: { const singleAgentToText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
',': CM_ERROR, ',': CM_ERROR,
':': {type: 'operator', suggest: true, then: {'': textToEnd}}, ':': colonTextToEnd,
}}; }};
const agentToOptText = {type: 'variable', suggest: 'Agent', then: { const agentToOptText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
@ -97,7 +102,7 @@ define(['core/ArrayUtilities'], (array) => {
}; };
} }
function makeOpBlock(exit) { function makeOpBlock(exit, sourceExit) {
const op = {type: 'operator', suggest: true, then: { const op = {type: 'operator', suggest: true, then: {
'+': CM_ERROR, '+': CM_ERROR,
'-': CM_ERROR, '-': CM_ERROR,
@ -126,13 +131,13 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
'': exit, '': exit,
}}, }},
'*': {type: 'operator', suggest: true, then: { '*': {type: 'operator', suggest: true, then: Object.assign({
'+': op, '+': op,
'-': op, '-': op,
'*': CM_ERROR, '*': CM_ERROR,
'!': CM_ERROR, '!': CM_ERROR,
'': exit, '': exit,
}}, }, sourceExit)},
'!': op, '!': op,
'': exit, '': exit,
}; };
@ -142,7 +147,10 @@ define(['core/ArrayUtilities'], (array) => {
const connect = { const connect = {
type: 'keyword', type: 'keyword',
suggest: true, suggest: true,
then: makeOpBlock(agentToOptText), then: makeOpBlock(agentToOptText, {
':': colonTextToEnd,
'\n': hiddenEnd,
}),
}; };
const then = {'': 0}; const then = {'': 0};
@ -153,7 +161,10 @@ define(['core/ArrayUtilities'], (array) => {
override: 'Label', override: 'Label',
then: {}, then: {},
}; };
return makeOpBlock({type: 'variable', suggest: 'Agent', then}); return makeOpBlock(
{type: 'variable', suggest: 'Agent', then},
then
);
} }
const BASE_THEN = { const BASE_THEN = {

View File

@ -36,8 +36,8 @@ define(['core/ArrayUtilities'], (array) => {
equals: (a, b) => { equals: (a, b) => {
return a.id === b.id; return a.id === b.id;
}, },
make: (id, {anchorRight = false} = {}) => { make: (id, {anchorRight = false, isVirtualSource = false} = {}) => {
return {id, anchorRight}; return {id, anchorRight, isVirtualSource};
}, },
indexOf: (list, gAgent) => { indexOf: (list, gAgent) => {
return array.indexOf(list, gAgent, GAgent.equals); return array.indexOf(list, gAgent, GAgent.equals);
@ -45,6 +45,14 @@ define(['core/ArrayUtilities'], (array) => {
hasIntersection: (a, b) => { hasIntersection: (a, b) => {
return array.hasIntersection(a, b, GAgent.equals); return array.hasIntersection(a, b, GAgent.equals);
}, },
addNearby: (target, reference, item, offset) => {
const p = array.indexOf(target, reference, GAgent.equals);
if(p === -1) {
target.push(item);
} else {
target.splice(p + offset, 0, item);
}
},
}; };
const NOTE_DEFAULT_G_AGENTS = { const NOTE_DEFAULT_G_AGENTS = {
@ -53,6 +61,8 @@ define(['core/ArrayUtilities'], (array) => {
'note right': [GAgent.make(']')], 'note right': [GAgent.make(']')],
}; };
const SPECIAL_AGENT_IDS = ['[', ']'];
const MERGABLE = { const MERGABLE = {
'agent begin': { 'agent begin': {
check: ['mode'], check: ['mode'],
@ -227,7 +237,7 @@ define(['core/ArrayUtilities'], (array) => {
this.activeGroups = new Map(); this.activeGroups = new Map();
this.gAgents = []; this.gAgents = [];
this.labelPattern = null; this.labelPattern = null;
this.blockCount = 0; this.nextID = 0;
this.nesting = []; this.nesting = [];
this.markers = new Set(); this.markers = new Set();
this.currentSection = null; this.currentSection = null;
@ -256,7 +266,7 @@ define(['core/ArrayUtilities'], (array) => {
this.endGroup = this.endGroup.bind(this); this.endGroup = this.endGroup.bind(this);
} }
toGAgent({alias, name}) { toGAgent({name, alias, flags}) {
if(alias) { if(alias) {
if(this.agentAliases.has(name)) { if(this.agentAliases.has(name)) {
throw new Error( throw new Error(
@ -275,7 +285,9 @@ define(['core/ArrayUtilities'], (array) => {
} }
this.agentAliases.set(alias, name); this.agentAliases.set(alias, name);
} }
return GAgent.make(this.agentAliases.get(name) || name); return GAgent.make(this.agentAliases.get(name) || name, {
isVirtualSource: flags.includes('source'),
});
} }
addStage(stage, isVisible = true) { addStage(stage, isVisible = true) {
@ -311,7 +323,12 @@ define(['core/ArrayUtilities'], (array) => {
} }
defineGAgents(gAgents) { defineGAgents(gAgents) {
array.mergeSets(this.currentNest.gAgents, gAgents, GAgent.equals); array.mergeSets(
this.currentNest.gAgents,
gAgents.filter((gAgent) =>
!SPECIAL_AGENT_IDS.includes(gAgent.id)),
GAgent.equals
);
array.mergeSets(this.gAgents, gAgents, GAgent.equals); array.mergeSets(this.gAgents, gAgents, GAgent.equals);
} }
@ -335,7 +352,9 @@ define(['core/ArrayUtilities'], (array) => {
validateGAgents(gAgents, { validateGAgents(gAgents, {
allowGrouped = false, allowGrouped = false,
rejectGrouped = false, rejectGrouped = false,
allowVirtual = false,
} = {}) { } = {}) {
/* jshint -W074 */ // agent validity checking requires several steps
gAgents.forEach((gAgent) => { gAgents.forEach((gAgent) => {
const state = this.getGAgentState(gAgent); const state = this.getGAgentState(gAgent);
if(state.covered) { if(state.covered) {
@ -349,6 +368,9 @@ define(['core/ArrayUtilities'], (array) => {
if(state.blocked && (!allowGrouped || state.group === null)) { if(state.blocked && (!allowGrouped || state.group === null)) {
throw new Error('Duplicate agent name: ' + gAgent.id); throw new Error('Duplicate agent name: ' + gAgent.id);
} }
if(!allowVirtual && gAgent.isVirtualSource) {
throw new Error('cannot use message source here');
}
if(gAgent.id.startsWith('__')) { if(gAgent.id.startsWith('__')) {
throw new Error(gAgent.id + ' is a reserved name'); throw new Error(gAgent.id + ' is a reserved name');
} }
@ -442,12 +464,18 @@ define(['core/ArrayUtilities'], (array) => {
this.replaceGAgentState(rightGAgent, AgentState.LOCKED); this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
this.nesting.push(this.currentNest); this.nesting.push(this.currentNest);
return {gAgents, stages}; return {stages};
} }
nextBlockName() { nextBlockName() {
const name = '__BLOCK' + this.blockCount; const name = '__BLOCK' + this.nextID;
++ this.blockCount; ++ this.nextID;
return name;
}
nextVirtualAgentName() {
const name = '__' + this.nextID;
++ this.nextID;
return name; return name;
} }
@ -651,9 +679,13 @@ define(['core/ArrayUtilities'], (array) => {
let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]); let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]);
let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]); let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]);
if(ind1 === -1) { if(ind1 === -1) {
ind1 = this.gAgents.length; // Virtual sources written as '* -> Ref' will spawn to the left,
// not the right (as non-virtual agents would)
ind1 = gAgents1[0].isVirtualSource ? -1 : this.gAgents.length;
} }
if(ind2 === -1) { if(ind2 === -1) {
// Virtual and non-virtual agents written as 'Ref -> *' will
// spawn to the right
ind2 = this.gAgents.length; ind2 = this.gAgents.length;
} }
if(ind1 === ind2) { if(ind1 === ind2) {
@ -700,21 +732,79 @@ define(['core/ArrayUtilities'], (array) => {
return {beginGAgents, endGAgents, startGAgents, stopGAgents}; return {beginGAgents, endGAgents, startGAgents, stopGAgents};
} }
makeVirtualAgent(anchorRight) {
const virtualGAgent = GAgent.make(this.nextVirtualAgentName(), {
anchorRight,
isVirtualSource: true,
});
this.replaceGAgentState(virtualGAgent, AgentState.LOCKED);
return virtualGAgent;
}
addNearbyAgent(gAgentReference, gAgent, offset) {
GAgent.addNearby(
this.currentNest.gAgents,
gAgentReference,
gAgent,
offset
);
GAgent.addNearby(
this.gAgents,
gAgentReference,
gAgent,
offset
);
}
expandVirtualSourceAgents(gAgents) {
if(gAgents[0].isVirtualSource) {
if(gAgents[1].isVirtualSource) {
throw new Error('Cannot connect found messages');
}
if(SPECIAL_AGENT_IDS.includes(gAgents[1].id)) {
throw new Error(
'Cannot connect found messages to special agents'
);
}
const virtualGAgent = this.makeVirtualAgent(true);
this.addNearbyAgent(gAgents[1], virtualGAgent, 0);
return [virtualGAgent, gAgents[1]];
}
if(gAgents[1].isVirtualSource) {
if(SPECIAL_AGENT_IDS.includes(gAgents[0].id)) {
throw new Error(
'Cannot connect found messages to special agents'
);
}
const virtualGAgent = this.makeVirtualAgent(false);
this.addNearbyAgent(gAgents[0], virtualGAgent, 1);
return [gAgents[0], virtualGAgent];
}
return gAgents;
}
handleConnect({agents, label, options}) { handleConnect({agents, label, options}) {
const flags = this.filterConnectFlags(agents); const flags = this.filterConnectFlags(agents);
let gAgents = agents.map(this.toGAgent); let gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents, {allowGrouped: true}); this.validateGAgents(gAgents, {
allowGrouped: true,
allowVirtual: true,
});
const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent); const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
this.defineGAgents(allGAgents); this.defineGAgents(allGAgents
.filter((gAgent) => !gAgent.isVirtualSource)
);
gAgents = this.expandGroupedGAgentConnection(gAgents); gAgents = this.expandGroupedGAgentConnection(gAgents);
gAgents = this.expandVirtualSourceAgents(gAgents);
const agentIDs = gAgents.map((gAgent) => gAgent.id); const agentIDs = gAgents.map((gAgent) => gAgent.id);
const implicitBeginGAgents = (agents const implicitBeginGAgents = (agents
.filter(PAgent.hasFlag('begin', false)) .filter(PAgent.hasFlag('begin', false))
.map(this.toGAgent) .map(this.toGAgent)
.filter((gAgent) => !gAgent.isVirtualSource)
); );
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
@ -811,7 +901,7 @@ define(['core/ArrayUtilities'], (array) => {
this.agentAliases.clear(); this.agentAliases.clear();
this.activeGroups.clear(); this.activeGroups.clear();
this.gAgents.length = 0; this.gAgents.length = 0;
this.blockCount = 0; this.nextID = 0;
this.nesting.length = 0; this.nesting.length = 0;
this.labelPattern = [{token: 'label'}]; this.labelPattern = [{token: 'label'}];
} }

View File

@ -110,6 +110,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
}; };
const GENERATED = { const GENERATED = {
agent: (id, {
formattedLabel = any(),
anchorRight = any(),
isVirtualSource = any(),
} = {}) => {
return {id, formattedLabel, anchorRight, isVirtualSource};
},
beginAgents: (agentIDs, { beginAgents: (agentIDs, {
mode = any(), mode = any(),
ln = any(), ln = any(),
@ -264,8 +272,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('includes implicit hidden left/right agents', () => { it('includes implicit hidden left/right agents', () => {
const sequence = invoke([]); const sequence = invoke([]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('[', {anchorRight: true}),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']', {anchorRight: false}),
]); ]);
}); });
@ -296,13 +304,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.beginAgents(['E']), PARSED.beginAgents(['E']),
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: 'D', formattedLabel: any(), anchorRight: false}, GENERATED.agent('D'),
{id: 'E', formattedLabel: any(), anchorRight: false}, GENERATED.agent('E'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -311,10 +319,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: any()}, GENERATED.agent('['),
{id: 'A', formattedLabel: 'A!', anchorRight: any()}, GENERATED.agent('A', {formattedLabel: 'A!'}),
{id: 'B', formattedLabel: 'B!', anchorRight: any()}, GENERATED.agent('B', {formattedLabel: 'B!'}),
{id: ']', formattedLabel: any(), anchorRight: any()}, GENERATED.agent(']'),
]); ]);
}); });
@ -323,9 +331,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect([']', 'B']), PARSED.connect([']', 'B']),
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -335,10 +343,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -348,10 +356,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'Baz', formattedLabel: any(), anchorRight: false}, GENERATED.agent('Baz'),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -419,6 +427,119 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
}); });
it('converts source agents into virtual agents', () => {
const sequence = invoke([
PARSED.connect([
'A',
{name: '', alias: '', flags: ['source']},
]),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__0', {
anchorRight: false,
isVirtualSource: true,
}),
GENERATED.agent(']'),
]);
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
GENERATED.connect(['A', '__0']),
GENERATED.endAgents(['A']),
]);
});
it('converts sources into distinct virtual agents', () => {
const sequence = invoke([
PARSED.connect([
'A',
{name: '', alias: '', flags: ['source']},
]),
PARSED.connect([
'A',
{name: '', alias: '', flags: ['source']},
]),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__1'),
GENERATED.agent('__0'),
GENERATED.agent(']'),
]);
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
GENERATED.connect(['A', '__0']),
GENERATED.connect(['A', '__1']),
GENERATED.endAgents(['A']),
]);
});
it('places source agents near the connected agent', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.connect([
'B',
{name: '', alias: '', flags: ['source']},
]),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('__0', {
anchorRight: false,
isVirtualSource: true,
}),
GENERATED.agent('C'),
GENERATED.agent(']'),
]);
});
it('places source agents left when connections are reversed', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.connect([
{name: '', alias: '', flags: ['source']},
'B',
]),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__0', {
anchorRight: true,
isVirtualSource: true,
}),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent(']'),
]);
});
it('rejects connections between virtual agents', () => {
expect(() => invoke([
PARSED.connect([
{name: '', alias: '', flags: ['source']},
{name: '', alias: '', flags: ['source']},
]),
])).toThrow(new Error(
'Cannot connect found messages at line 1'
));
});
it('rejects connections between virtual agents and sides', () => {
expect(() => invoke([
PARSED.connect([
{name: '', alias: '', flags: ['source']},
']',
]),
])).toThrow(new Error(
'Cannot connect found messages to special agents at line 1'
));
});
it('uses label patterns for connections', () => { it('uses label patterns for connections', () => {
const sequence = invoke([ const sequence = invoke([
PARSED.labelPattern(['foo ', {token: 'label'}, ' bar']), PARSED.labelPattern(['foo ', {token: 'label'}, ' bar']),
@ -819,12 +940,12 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0[', {anchorRight: true}),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]', {anchorRight: false}),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -841,20 +962,40 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: 'D', formattedLabel: any(), anchorRight: false}, GENERATED.agent('D'),
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK1['),
{id: 'E', formattedLabel: any(), anchorRight: false}, GENERATED.agent('E'),
{id: 'F', formattedLabel: any(), anchorRight: false}, GENERATED.agent('F'),
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK1]'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: 'G', formattedLabel: any(), anchorRight: false}, GENERATED.agent('G'),
{id: 'H', formattedLabel: any(), anchorRight: false}, GENERATED.agent('H'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]);
});
it('ignores side agents when calculating block bounds', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.blockBegin('if', 'abc'),
PARSED.connect(['[', 'B']),
PARSED.connect(['B', ']']),
PARSED.blockEnd(),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('B'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('C'),
GENERATED.agent(']'),
]); ]);
}); });
@ -916,15 +1057,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK1['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK1]'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
const bounds0 = { const bounds0 = {
@ -955,14 +1096,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK1['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK1]'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
const bounds0 = { const bounds0 = {
@ -1051,12 +1192,12 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
}; };
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
@ -1116,14 +1257,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: 'D', formattedLabel: any(), anchorRight: false}, GENERATED.agent('D'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -1135,15 +1276,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: 'D', formattedLabel: any(), anchorRight: false}, GENERATED.agent('D'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: 'E', formattedLabel: any(), anchorRight: false}, GENERATED.agent('E'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
}); });
@ -1342,16 +1483,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequenceR.agents).toEqual([ expect(sequenceR.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK1['),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0[', {anchorRight: true}),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]', {anchorRight: false}),
{id: 'D', formattedLabel: any(), anchorRight: false}, GENERATED.agent('D'),
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK1]'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]); ]);
const sequenceL = invoke([ const sequenceL = invoke([
@ -1364,16 +1505,51 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
expect(sequenceL.agents).toEqual([ expect(sequenceL.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('['),
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK1['),
{id: 'A', formattedLabel: any(), anchorRight: false}, GENERATED.agent('A'),
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true}, GENERATED.agent('__BLOCK0['),
{id: 'B', formattedLabel: any(), anchorRight: false}, GENERATED.agent('B'),
{id: 'C', formattedLabel: any(), anchorRight: false}, GENERATED.agent('C'),
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK0]'),
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false}, GENERATED.agent('__BLOCK1]'),
{id: 'D', formattedLabel: any(), anchorRight: false}, GENERATED.agent('D'),
{id: ']', formattedLabel: any(), anchorRight: false}, GENERATED.agent(']'),
]);
});
it('allows connections between sources and references', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C', 'D']),
PARSED.groupBegin('Bar', ['B', 'C'], {label: 'Foo'}),
PARSED.connect([
{name: '', alias: '', flags: ['source']},
'Bar',
]),
PARSED.connect([
'Bar',
{name: '', alias: '', flags: ['source']},
]),
PARSED.endAgents(['Bar']),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__1', {
anchorRight: true,
isVirtualSource: true,
}),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('__2', {
anchorRight: false,
isVirtualSource: true,
}),
GENERATED.agent('D'),
GENERATED.agent(']'),
]); ]);
}); });

View File

@ -67,10 +67,10 @@ define([
})()); })());
const CONNECT_AGENT_FLAGS = { const CONNECT_AGENT_FLAGS = {
'*': 'begin', '*': {flag: 'begin', allowBlankName: true, blankNameFlag: 'source'},
'+': 'start', '+': {flag: 'start'},
'-': 'stop', '-': {flag: 'stop'},
'!': 'end', '!': {flag: 'end'},
}; };
const TERMINATOR_TYPES = [ const TERMINATOR_TYPES = [
@ -182,7 +182,7 @@ define([
return -1; return -1;
} }
function readAgentAlias(line, start, end, enableAlias) { function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
let aliasSep = -1; let aliasSep = -1;
if(enableAlias) { if(enableAlias) {
aliasSep = findToken(line, 'as', start); aliasSep = findToken(line, 'as', start);
@ -190,7 +190,7 @@ define([
if(aliasSep === -1 || aliasSep >= end) { if(aliasSep === -1 || aliasSep >= end) {
aliasSep = end; aliasSep = end;
} }
if(start >= aliasSep) { if(start >= aliasSep && !allowBlankName) {
throw makeError('Missing agent name', errToken(line, start)); throw makeError('Missing agent name', errToken(line, start));
} }
return { return {
@ -204,25 +204,31 @@ define([
aliases = false, aliases = false,
} = {}) { } = {}) {
const flags = []; const flags = [];
const blankNameFlags = [];
let p = start; let p = start;
let allowBlankName = false;
for(; p < end; ++ p) { for(; p < end; ++ p) {
const token = line[p]; const token = line[p];
const rawFlag = tokenKeyword(token); const rawFlag = tokenKeyword(token);
const flag = flagTypes[rawFlag]; const flag = flagTypes[rawFlag];
if(flag) { if(!flag) {
if(flags.includes(flag)) {
throw makeError('Duplicate agent flag: ' + rawFlag, token);
}
flags.push(flag);
} else {
break; break;
} }
if(flags.includes(flag.flag)) {
throw makeError('Duplicate agent flag: ' + rawFlag, token);
} }
const {name, alias} = readAgentAlias(line, p, end, aliases); allowBlankName = allowBlankName || Boolean(flag.allowBlankName);
flags.push(flag.flag);
blankNameFlags.push(flag.blankNameFlag);
}
const {name, alias} = readAgentAlias(line, p, end, {
enableAlias: aliases,
allowBlankName,
});
return { return {
name, name,
alias, alias,
flags, flags: name ? flags : blankNameFlags,
}; };
} }

View File

@ -185,6 +185,38 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
)); ));
}); });
it('parses source agents', () => {
const parsed = parser.parse('A -> *');
expect(parsed.stages).toEqual([
{
type: 'connect',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: '', alias: '', flags: ['source']},
],
label: jasmine.anything(),
options: jasmine.anything(),
},
]);
});
it('parses source agents with labels', () => {
const parsed = parser.parse('A -> *: foo');
expect(parsed.stages).toEqual([
{
type: 'connect',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: '', alias: '', flags: ['source']},
],
label: 'foo',
options: jasmine.anything(),
},
]);
});
it('converts multiple entries', () => { it('converts multiple entries', () => {
const parsed = parser.parse('A -> B\nB -> A'); const parsed = parser.parse('A -> B\nB -> A');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([

View File

@ -188,6 +188,7 @@ define([
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
visibleAgentIDs: this.visibleAgentIDs, visibleAgentIDs: this.visibleAgentIDs,
momentaryAgentIDs: agentIDs,
textSizer: this.sizer, textSizer: this.sizer,
addSpacing, addSpacing,
addSeparation: this.addSeparation, addSeparation: this.addSeparation,
@ -392,6 +393,7 @@ define([
id: agent.id, id: agent.id,
formattedLabel: agent.formattedLabel, formattedLabel: agent.formattedLabel,
anchorRight: agent.anchorRight, anchorRight: agent.anchorRight,
isVirtualSource: agent.isVirtualSource,
index, index,
x: null, x: null,
latestYStart: null, latestYStart: null,

View File

@ -13,6 +13,7 @@ define(() => {
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
momentaryAgentIDs,
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
@ -25,6 +26,7 @@ define(() => {
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
momentaryAgentIDs,
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,

View File

@ -1,8 +1,10 @@
define([ define([
'core/ArrayUtilities',
'./BaseComponent', './BaseComponent',
'svg/SVGUtilities', 'svg/SVGUtilities',
'svg/SVGShapes', 'svg/SVGShapes',
], ( ], (
array,
BaseComponent, BaseComponent,
svg, svg,
SVGShapes SVGShapes
@ -106,6 +108,18 @@ define([
]; ];
class Connect extends BaseComponent { class Connect extends BaseComponent {
separationPre({agentIDs}, env) {
const r = env.theme.connect.source.radius;
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
if(!agentInfo.isVirtualSource) {
return;
}
agentInfo.currentRad = r;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
}
separation({label, agentIDs, options}, env) { separation({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
@ -147,6 +161,8 @@ define([
) * 2 ) * 2
); );
} }
array.mergeSets(env.momentaryAgentIDs, agentIDs);
} }
renderSelfConnect({label, agentIDs, options}, env) { renderSelfConnect({label, agentIDs, options}, env) {
@ -224,14 +240,59 @@ define([
); );
} }
renderSimpleLine(x0, x1, options, env) {
const dir = (x0 < x1) ? 1 : -1;
const config = env.theme.connect;
const line = config.line[options.line];
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
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,
});
env.shapeLayer.appendChild(rendered.shape);
return rendered;
}
renderSimpleArrowheads(options, renderedLine, env, dir) {
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
lArrow.render(env.shapeLayer, env.theme, renderedLine.p1, dir);
rArrow.render(env.shapeLayer, env.theme, renderedLine.p2, -dir);
return {lArrow, rArrow};
}
renderVirtualSources(from, to, renderedLine, env) {
const config = env.theme.connect.source;
if(from.isVirtualSource) {
env.shapeLayer.appendChild(config.render({
x: renderedLine.p1.x - config.radius,
y: renderedLine.p1.y,
radius: config.radius,
}));
}
if(to.isVirtualSource) {
env.shapeLayer.appendChild(config.render({
x: renderedLine.p2.x + config.radius,
y: renderedLine.p2.y,
radius: config.radius,
}));
}
}
renderSimpleConnect({label, agentIDs, options}, env) { renderSimpleConnect({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
const from = env.agentInfos.get(agentIDs[0]); const from = env.agentInfos.get(agentIDs[0]);
const to = env.agentInfos.get(agentIDs[1]); const to = env.agentInfos.get(agentIDs[1]);
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const dir = (from.x < to.x) ? 1 : -1; const dir = (from.x < to.x) ? 1 : -1;
const height = ( const height = (
@ -257,18 +318,12 @@ define([
SVGTextBlockClass: env.SVGTextBlockClass, SVGTextBlockClass: env.SVGTextBlockClass,
}); });
const line = config.line[options.line]; const rendered = this.renderSimpleLine(x0, x1, options, env);
const rendered = line.renderFlat(line.attrs, { const {
x1: x0, lArrow,
dx1: lArrow.lineGap(env.theme, line.attrs) * dir, rArrow
x2: x1, } = this.renderSimpleArrowheads(options, rendered, env, dir);
dx2: -rArrow.lineGap(env.theme, line.attrs) * dir, this.renderVirtualSources(from, to, rendered, env);
y,
});
env.shapeLayer.appendChild(rendered.shape);
lArrow.render(env.shapeLayer, env.theme, rendered.p1, dir);
rArrow.render(env.shapeLayer, env.theme, rendered.p2, -dir);
const arrowSpread = Math.max( const arrowSpread = Math.max(
lArrow.height(env.theme), lArrow.height(env.theme),

View File

@ -150,6 +150,19 @@ define([
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 2,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
});
},
},
mask: { mask: {
padding: { padding: {
top: 0, top: 0,

View File

@ -160,6 +160,19 @@ define([
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 5,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 3,
});
},
},
mask: { mask: {
padding: { padding: {
top: 1, top: 1,

View File

@ -157,6 +157,19 @@ define([
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 2,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
});
},
},
mask: { mask: {
padding: { padding: {
top: 0, top: 0,

View File

@ -139,6 +139,19 @@ define([
'line-height': LINE_HEIGHT, 'line-height': LINE_HEIGHT,
}, },
}, },
source: {
radius: 1,
render: ({x, y, radius}) => {
return svg.make('circle', {
'cx': x,
'cy': y,
'r': radius,
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
});
},
},
mask: { mask: {
padding: { padding: {
top: 0, top: 0,
@ -574,7 +587,7 @@ define([
); );
return { return {
shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)), shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)),
p1: {x: ln.p1.x - dx1, y: ln.p2.y}, p1: {x: ln.p1.x - dx1, y: ln.p1.y},
p2: {x: ln.p2.x - dx2, y: ln.p2.y}, p2: {x: ln.p2.x - dx2, y: ln.p2.y},
}; };
} }