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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@ -110,6 +110,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
};
const GENERATED = {
agent: (id, {
formattedLabel = any(),
anchorRight = any(),
isVirtualSource = any(),
} = {}) => {
return {id, formattedLabel, anchorRight, isVirtualSource};
},
beginAgents: (agentIDs, {
mode = any(),
ln = any(),
@ -264,8 +272,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('includes implicit hidden left/right agents', () => {
const sequence = invoke([]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('[', {anchorRight: true}),
GENERATED.agent(']', {anchorRight: false}),
]);
});
@ -296,13 +304,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.beginAgents(['E']),
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: 'D', formattedLabel: any(), anchorRight: false},
{id: 'E', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('D'),
GENERATED.agent('E'),
GENERATED.agent(']'),
]);
});
@ -311,10 +319,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['A', 'B']),
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: any()},
{id: 'A', formattedLabel: 'A!', anchorRight: any()},
{id: 'B', formattedLabel: 'B!', anchorRight: any()},
{id: ']', formattedLabel: any(), anchorRight: any()},
GENERATED.agent('['),
GENERATED.agent('A', {formattedLabel: 'A!'}),
GENERATED.agent('B', {formattedLabel: 'B!'}),
GENERATED.agent(']'),
]);
});
@ -323,9 +331,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect([']', 'B']),
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('B'),
GENERATED.agent(']'),
]);
});
@ -335,10 +343,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['A', 'B']),
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('B'),
GENERATED.agent('A'),
GENERATED.agent(']'),
]);
});
@ -348,10 +356,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['A', 'B']),
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'Baz', formattedLabel: any(), anchorRight: false},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('Baz'),
GENERATED.agent('A'),
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', () => {
const sequence = invoke([
PARSED.labelPattern(['foo ', {token: 'label'}, ' bar']),
@ -819,12 +940,12 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('__BLOCK0[', {anchorRight: true}),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('__BLOCK0]', {anchorRight: false}),
GENERATED.agent(']'),
]);
});
@ -841,20 +962,40 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: 'D', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true},
{id: 'E', formattedLabel: any(), anchorRight: false},
{id: 'F', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: 'G', formattedLabel: any(), anchorRight: false},
{id: 'H', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('C'),
GENERATED.agent('D'),
GENERATED.agent('__BLOCK1['),
GENERATED.agent('E'),
GENERATED.agent('F'),
GENERATED.agent('__BLOCK1]'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('G'),
GENERATED.agent('H'),
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([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('__BLOCK1['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('__BLOCK1]'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent(']'),
]);
const bounds0 = {
@ -955,14 +1096,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('__BLOCK1['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('__BLOCK1]'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent(']'),
]);
const bounds0 = {
@ -1051,12 +1192,12 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
};
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent(']'),
]);
expect(sequence.stages).toEqual([
@ -1116,14 +1257,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: 'D', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('D'),
GENERATED.agent(']'),
]);
});
@ -1135,15 +1276,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequence.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: 'D', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: 'E', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('D'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('E'),
GENERATED.agent(']'),
]);
});
@ -1342,16 +1483,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequenceR.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: 'D', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('A'),
GENERATED.agent('__BLOCK1['),
GENERATED.agent('__BLOCK0[', {anchorRight: true}),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('__BLOCK0]', {anchorRight: false}),
GENERATED.agent('D'),
GENERATED.agent('__BLOCK1]'),
GENERATED.agent(']'),
]);
const sequenceL = invoke([
@ -1364,16 +1505,51 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
expect(sequenceL.agents).toEqual([
{id: '[', formattedLabel: any(), anchorRight: true},
{id: '__BLOCK1[', formattedLabel: any(), anchorRight: true},
{id: 'A', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0[', formattedLabel: any(), anchorRight: true},
{id: 'B', formattedLabel: any(), anchorRight: false},
{id: 'C', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK0]', formattedLabel: any(), anchorRight: false},
{id: '__BLOCK1]', formattedLabel: any(), anchorRight: false},
{id: 'D', formattedLabel: any(), anchorRight: false},
{id: ']', formattedLabel: any(), anchorRight: false},
GENERATED.agent('['),
GENERATED.agent('__BLOCK1['),
GENERATED.agent('A'),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('B'),
GENERATED.agent('C'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('__BLOCK1]'),
GENERATED.agent('D'),
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 = {
'*': 'begin',
'+': 'start',
'-': 'stop',
'!': 'end',
'*': {flag: 'begin', allowBlankName: true, blankNameFlag: 'source'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
'!': {flag: 'end'},
};
const TERMINATOR_TYPES = [
@ -182,7 +182,7 @@ define([
return -1;
}
function readAgentAlias(line, start, end, enableAlias) {
function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
let aliasSep = -1;
if(enableAlias) {
aliasSep = findToken(line, 'as', start);
@ -190,7 +190,7 @@ define([
if(aliasSep === -1 || aliasSep >= end) {
aliasSep = end;
}
if(start >= aliasSep) {
if(start >= aliasSep && !allowBlankName) {
throw makeError('Missing agent name', errToken(line, start));
}
return {
@ -204,25 +204,31 @@ define([
aliases = false,
} = {}) {
const flags = [];
const blankNameFlags = [];
let p = start;
let allowBlankName = false;
for(; p < end; ++ p) {
const token = line[p];
const rawFlag = tokenKeyword(token);
const flag = flagTypes[rawFlag];
if(flag) {
if(flags.includes(flag)) {
throw makeError('Duplicate agent flag: ' + rawFlag, token);
}
flags.push(flag);
} else {
if(!flag) {
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 {
name,
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', () => {
const parsed = parser.parse('A -> B\nB -> A');
expect(parsed.stages).toEqual([

View File

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

View File

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

View File

@ -1,8 +1,10 @@
define([
'core/ArrayUtilities',
'./BaseComponent',
'svg/SVGUtilities',
'svg/SVGShapes',
], (
array,
BaseComponent,
svg,
SVGShapes
@ -106,6 +108,18 @@ define([
];
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) {
const config = env.theme.connect;
@ -147,6 +161,8 @@ define([
) * 2
);
}
array.mergeSets(env.momentaryAgentIDs, agentIDs);
}
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) {
const config = env.theme.connect;
const from = env.agentInfos.get(agentIDs[0]);
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 height = (
@ -257,18 +318,12 @@ define([
SVGTextBlockClass: env.SVGTextBlockClass,
});
const line = config.line[options.line];
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.shapeLayer.appendChild(rendered.shape);
lArrow.render(env.shapeLayer, env.theme, rendered.p1, dir);
rArrow.render(env.shapeLayer, env.theme, rendered.p2, -dir);
const rendered = this.renderSimpleLine(x0, x1, options, env);
const {
lArrow,
rArrow
} = this.renderSimpleArrowheads(options, rendered, env, dir);
this.renderVirtualSources(from, to, rendered, env);
const arrowSpread = Math.max(
lArrow.height(env.theme),

View File

@ -150,6 +150,19 @@ define([
'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: {
padding: {
top: 0,

View File

@ -160,6 +160,19 @@ define([
'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: {
padding: {
top: 1,

View File

@ -157,6 +157,19 @@ define([
'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: {
padding: {
top: 0,

View File

@ -139,6 +139,19 @@ define([
'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: {
padding: {
top: 0,
@ -574,7 +587,7 @@ define([
);
return {
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},
};
}