Add support for agent line highlighting, also sets groundwork for parallel actions [#10]
This commit is contained in:
parent
dc3d930544
commit
71437d2576
|
@ -52,6 +52,9 @@ Foo -> Bar
|
|||
|
||||
Foo -> Foo: Foo talks to itself
|
||||
|
||||
Foo -> +Bar: Foo asks Bar
|
||||
-Bar --> Foo: and Bar replies
|
||||
|
||||
# Arrows leaving on the left and right of the diagram
|
||||
[ -> Foo: From the left
|
||||
[ <- Foo: To the left
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 49 KiB |
|
@ -24,6 +24,15 @@ define(() => {
|
|||
}
|
||||
}
|
||||
|
||||
function hasIntersection(a, b, equalityCheck = null) {
|
||||
for(let i = 0; i < b.length; ++ i) {
|
||||
if(indexOf(a, b[i], equalityCheck) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeAll(target, b = null, equalityCheck = null) {
|
||||
if(!b) {
|
||||
return;
|
||||
|
@ -50,6 +59,7 @@ define(() => {
|
|||
return {
|
||||
indexOf,
|
||||
mergeSets,
|
||||
hasIntersection,
|
||||
removeAll,
|
||||
remove,
|
||||
last,
|
||||
|
|
|
@ -65,6 +65,26 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('.hasIntersection', () => {
|
||||
it('returns true if any elements are shared between the sets', () => {
|
||||
const p1 = ['a', 'b'];
|
||||
const p2 = ['b', 'c'];
|
||||
expect(array.hasIntersection(p1, p2)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false if no elements are shared between the sets', () => {
|
||||
const p1 = ['a', 'b'];
|
||||
const p2 = ['c', 'd'];
|
||||
expect(array.hasIntersection(p1, p2)).toEqual(false);
|
||||
});
|
||||
|
||||
it('uses the given equality check function', () => {
|
||||
const p1 = ['a', 'b'];
|
||||
const p2 = ['B', 'c'];
|
||||
expect(array.hasIntersection(p1, p2, ignoreCase)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.removeAll', () => {
|
||||
it('removes elements from the first array', () => {
|
||||
const p1 = ['a', 'b', 'c'];
|
||||
|
|
|
@ -4,6 +4,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
class AgentState {
|
||||
constructor(visible, locked = false) {
|
||||
this.visible = visible;
|
||||
this.highlighted = false;
|
||||
this.locked = locked;
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +25,157 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
return agent.name;
|
||||
}
|
||||
|
||||
function agentHasFlag(flag) {
|
||||
return (agent) => agent.flags.includes(flag);
|
||||
}
|
||||
|
||||
const MERGABLE = {
|
||||
'agent begin': {
|
||||
check: ['mode'],
|
||||
merge: ['agentNames'],
|
||||
siblings: new Set(['agent highlight']),
|
||||
},
|
||||
'agent end': {
|
||||
check: ['mode'],
|
||||
merge: ['agentNames'],
|
||||
siblings: new Set(['agent highlight']),
|
||||
},
|
||||
'agent highlight': {
|
||||
check: ['highlighted'],
|
||||
merge: ['agentNames'],
|
||||
siblings: new Set(['agent begin', 'agent end']),
|
||||
},
|
||||
};
|
||||
|
||||
function mergableParallel(target, copy) {
|
||||
const info = MERGABLE[target.type];
|
||||
if(!info || target.type !== copy.type) {
|
||||
return false;
|
||||
}
|
||||
if(info.check.some((c) => target[c] !== copy[c])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function performMerge(target, copy) {
|
||||
const info = MERGABLE[target.type];
|
||||
info.merge.forEach((m) => {
|
||||
array.mergeSets(target[m], copy[m]);
|
||||
});
|
||||
}
|
||||
|
||||
function iterateRemoval(list, fn) {
|
||||
for(let i = 0; i < list.length;) {
|
||||
const remove = fn(list[i], i);
|
||||
if(remove) {
|
||||
list.splice(i, 1);
|
||||
} else {
|
||||
++ i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function performParallelMergers(stages) {
|
||||
iterateRemoval(stages, (stage, i) => {
|
||||
for(let j = 0; j < i; ++ j) {
|
||||
if(mergableParallel(stages[j], stage)) {
|
||||
performMerge(stages[j], stage);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function findViableSequentialMergers(stages) {
|
||||
const mergers = new Set();
|
||||
const types = stages.map(({type}) => type);
|
||||
types.forEach((type) => {
|
||||
const info = MERGABLE[type];
|
||||
if(!info) {
|
||||
return;
|
||||
}
|
||||
if(types.every((sType) =>
|
||||
(type === sType || info.siblings.has(sType))
|
||||
)) {
|
||||
mergers.add(type);
|
||||
}
|
||||
});
|
||||
return mergers;
|
||||
}
|
||||
|
||||
function performSequentialMergers(lastViable, viable, lastStages, stages) {
|
||||
iterateRemoval(stages, (stage) => {
|
||||
if(!lastViable.has(stage.type) || !viable.has(stage.type)) {
|
||||
return false;
|
||||
}
|
||||
for(let j = 0; j < lastStages.length; ++ j) {
|
||||
if(mergableParallel(lastStages[j], stage)) {
|
||||
performMerge(lastStages[j], stage);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function optimiseStages(stages) {
|
||||
let lastStages = [];
|
||||
let lastViable = new Set();
|
||||
for(let i = 0; i < stages.length;) {
|
||||
const stage = stages[i];
|
||||
let subStages = null;
|
||||
if(stage.type === 'parallel') {
|
||||
subStages = stage.stages;
|
||||
} else {
|
||||
subStages = [stage];
|
||||
}
|
||||
|
||||
performParallelMergers(subStages);
|
||||
const viable = findViableSequentialMergers(subStages);
|
||||
performSequentialMergers(lastViable, viable, lastStages, subStages);
|
||||
|
||||
lastViable = viable;
|
||||
lastStages = subStages;
|
||||
|
||||
if(subStages.length === 0) {
|
||||
stages.splice(i, 1);
|
||||
} else if(stage.type === 'parallel' && subStages.length === 1) {
|
||||
stages.splice(i, 1, subStages[0]);
|
||||
++ i;
|
||||
} else {
|
||||
++ i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addBounds(target, agentL, agentR, involvedAgents = null) {
|
||||
array.remove(target, agentL, agentEqCheck);
|
||||
array.remove(target, agentR, agentEqCheck);
|
||||
|
||||
let indexL = 0;
|
||||
let indexR = target.length;
|
||||
if(involvedAgents) {
|
||||
const found = (involvedAgents
|
||||
.map((agent) => array.indexOf(target, agent, agentEqCheck))
|
||||
.filter((p) => (p !== -1))
|
||||
);
|
||||
indexL = found.reduce((a, b) => Math.min(a, b), target.length);
|
||||
indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1;
|
||||
}
|
||||
|
||||
target.splice(indexL, 0, agentL);
|
||||
target.splice(indexR + 1, 0, agentR);
|
||||
}
|
||||
|
||||
const LOCKED_AGENT = new AgentState(false, true);
|
||||
const DEFAULT_AGENT = new AgentState(false);
|
||||
|
||||
const NOTE_DEFAULT_AGENTS = {
|
||||
'note over': [{name: '['}, {name: ']'}],
|
||||
'note left': [{name: '['}],
|
||||
'note right': [{name: ']'}],
|
||||
'note over': [{name: '[', flags: []}, {name: ']', flags: []}],
|
||||
'note left': [{name: '[', flags: []}],
|
||||
'note right': [{name: ']', flags: []}],
|
||||
};
|
||||
|
||||
return class Generator {
|
||||
|
@ -61,32 +206,30 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
this.handleStage = this.handleStage.bind(this);
|
||||
}
|
||||
|
||||
addBounds(target, agentL, agentR, involvedAgents = null) {
|
||||
array.remove(target, agentL, agentEqCheck);
|
||||
array.remove(target, agentR, agentEqCheck);
|
||||
|
||||
let indexL = 0;
|
||||
let indexR = target.length;
|
||||
if(involvedAgents) {
|
||||
const found = (involvedAgents
|
||||
.map((agent) => array.indexOf(target, agent, agentEqCheck))
|
||||
.filter((p) => (p !== -1))
|
||||
);
|
||||
indexL = found.reduce((a, b) => Math.min(a, b), target.length);
|
||||
indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1;
|
||||
}
|
||||
|
||||
target.splice(indexL, 0, agentL);
|
||||
target.splice(indexR + 1, 0, agentR);
|
||||
}
|
||||
|
||||
addStage(stage, isVisible = true) {
|
||||
if(!stage) {
|
||||
return;
|
||||
}
|
||||
this.currentSection.stages.push(stage);
|
||||
if(isVisible) {
|
||||
this.currentNest.hasContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
addParallelStages(stages) {
|
||||
const viableStages = stages.filter((stage) => Boolean(stage));
|
||||
if(viableStages.length === 0) {
|
||||
return;
|
||||
}
|
||||
if(viableStages.length === 1) {
|
||||
return this.addStage(viableStages[0]);
|
||||
}
|
||||
return this.addStage({
|
||||
type: 'parallel',
|
||||
stages: viableStages,
|
||||
});
|
||||
}
|
||||
|
||||
defineAgents(agents) {
|
||||
array.mergeSets(this.currentNest.agents, agents, agentEqCheck);
|
||||
array.mergeSets(this.agents, agents, agentEqCheck);
|
||||
|
@ -97,7 +240,9 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
|
||||
if(state.locked) {
|
||||
if(checked) {
|
||||
throw new Error('Cannot begin/end agent: ' + agent);
|
||||
throw new Error(
|
||||
'Cannot begin/end agent: ' + agent.name
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -105,7 +250,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
return state.visible !== visible;
|
||||
});
|
||||
if(filteredAgents.length === 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
filteredAgents.forEach((agent) => {
|
||||
const state = this.agentStates.get(agent.name);
|
||||
|
@ -115,19 +260,42 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
this.agentStates.set(agent.name, new AgentState(visible));
|
||||
}
|
||||
});
|
||||
const type = (visible ? 'agent begin' : 'agent end');
|
||||
const existing = array.last(this.currentSection.stages) || {};
|
||||
const agentNames = filteredAgents.map(getAgentName);
|
||||
if(existing.type === type && existing.mode === mode) {
|
||||
array.mergeSets(existing.agentNames, agentNames);
|
||||
} else {
|
||||
this.addStage({
|
||||
type,
|
||||
agentNames,
|
||||
mode,
|
||||
});
|
||||
}
|
||||
this.defineAgents(filteredAgents);
|
||||
|
||||
return {
|
||||
type: (visible ? 'agent begin' : 'agent end'),
|
||||
agentNames: filteredAgents.map(getAgentName),
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
setAgentHighlight(agents, highlighted, checked = false) {
|
||||
const filteredAgents = agents.filter((agent) => {
|
||||
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
|
||||
if(state.locked) {
|
||||
if(checked) {
|
||||
throw new Error(
|
||||
'Cannot highlight agent: ' + agent.name
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return state.visible && (state.highlighted !== highlighted);
|
||||
});
|
||||
if(filteredAgents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
filteredAgents.forEach((agent) => {
|
||||
const state = this.agentStates.get(agent.name);
|
||||
state.highlighted = highlighted;
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'agent highlight',
|
||||
agentNames: filteredAgents.map(getAgentName),
|
||||
highlighted,
|
||||
};
|
||||
}
|
||||
|
||||
beginNested(mode, label, name) {
|
||||
|
@ -173,15 +341,26 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
|
||||
handleConnect({agents, label, options}) {
|
||||
const colAgents = agents.map(convertAgent);
|
||||
this.setAgentVis(colAgents, true, 'box');
|
||||
this.addStage(this.setAgentVis(colAgents, true, 'box'));
|
||||
this.defineAgents(colAgents);
|
||||
|
||||
this.addStage({
|
||||
const startAgents = agents.filter(agentHasFlag('start'));
|
||||
const stopAgents = agents.filter(agentHasFlag('stop'));
|
||||
if(array.hasIntersection(startAgents, stopAgents, agentEqCheck)) {
|
||||
throw new Error('Cannot set agent highlighting multiple times');
|
||||
}
|
||||
const connectStage = {
|
||||
type: 'connect',
|
||||
agentNames: agents.map(getAgentName),
|
||||
label,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
this.addParallelStages([
|
||||
this.setAgentHighlight(startAgents, true, true),
|
||||
connectStage,
|
||||
this.setAgentHighlight(stopAgents, false, true),
|
||||
]);
|
||||
}
|
||||
|
||||
handleNote({type, agents, mode, label}) {
|
||||
|
@ -192,7 +371,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
colAgents = agents.map(convertAgent);
|
||||
}
|
||||
|
||||
this.setAgentVis(colAgents, true, 'box');
|
||||
this.addStage(this.setAgentVis(colAgents, true, 'box'));
|
||||
this.defineAgents(colAgents);
|
||||
|
||||
this.addStage({
|
||||
|
@ -209,11 +388,19 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
|
||||
handleAgentBegin({agents, mode}) {
|
||||
this.setAgentVis(agents.map(convertAgent), true, mode, true);
|
||||
this.addStage(this.setAgentVis(
|
||||
agents.map(convertAgent),
|
||||
true,
|
||||
mode,
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
handleAgentEnd({agents, mode}) {
|
||||
this.setAgentVis(agents.map(convertAgent), false, mode, true);
|
||||
this.addParallelStages([
|
||||
this.setAgentHighlight(agents, false),
|
||||
this.setAgentVis(agents.map(convertAgent), false, mode, true),
|
||||
]);
|
||||
}
|
||||
|
||||
handleBlockBegin({mode, label}) {
|
||||
|
@ -230,6 +417,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
containerMode + ')'
|
||||
);
|
||||
}
|
||||
optimiseStages(this.currentSection.stages);
|
||||
this.currentSection = {
|
||||
mode,
|
||||
label,
|
||||
|
@ -242,12 +430,13 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
if(this.nesting.length <= 1) {
|
||||
throw new Error('Invalid block nesting (too many "end"s)');
|
||||
}
|
||||
optimiseStages(this.currentSection.stages);
|
||||
const nested = this.nesting.pop();
|
||||
this.currentNest = array.last(this.nesting);
|
||||
this.currentSection = array.last(this.currentNest.stage.sections);
|
||||
if(nested.hasContent) {
|
||||
this.defineAgents(nested.agents);
|
||||
this.addBounds(
|
||||
addBounds(
|
||||
this.agents,
|
||||
nested.leftAgent,
|
||||
nested.rightAgent,
|
||||
|
@ -278,13 +467,19 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
);
|
||||
}
|
||||
|
||||
this.setAgentVis(this.agents, false, meta.terminators || 'none');
|
||||
const terminators = meta.terminators || 'none';
|
||||
|
||||
this.addBounds(
|
||||
this.addParallelStages([
|
||||
this.setAgentHighlight(this.agents, false),
|
||||
this.setAgentVis(this.agents, false, terminators),
|
||||
]);
|
||||
|
||||
addBounds(
|
||||
this.agents,
|
||||
this.currentNest.leftAgent,
|
||||
this.currentNest.rightAgent
|
||||
);
|
||||
optimiseStages(globals.stages);
|
||||
|
||||
return {
|
||||
meta: {
|
||||
|
@ -296,4 +491,3 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -3,6 +3,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
|
||||
const generator = new Generator();
|
||||
|
||||
function makeParsedAgents(source) {
|
||||
return source.map((item) => {
|
||||
if(typeof item === 'object') {
|
||||
return item;
|
||||
} else {
|
||||
return {name: item, flags: []};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const PARSED = {
|
||||
blockBegin: (mode, label) => {
|
||||
return {type: 'block begin', mode, label};
|
||||
|
@ -19,14 +29,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
defineAgents: (agentNames) => {
|
||||
return {
|
||||
type: 'agent define',
|
||||
agents: agentNames.map((name) => ({name, flags: []})),
|
||||
agents: makeParsedAgents(agentNames),
|
||||
};
|
||||
},
|
||||
|
||||
beginAgents: (agentNames, {mode = 'box'} = {}) => {
|
||||
return {
|
||||
type: 'agent begin',
|
||||
agents: agentNames.map((name) => ({name, flags: []})),
|
||||
agents: makeParsedAgents(agentNames),
|
||||
mode,
|
||||
};
|
||||
},
|
||||
|
@ -34,7 +44,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
endAgents: (agentNames, {mode = 'cross'} = {}) => {
|
||||
return {
|
||||
type: 'agent end',
|
||||
agents: agentNames.map((name) => ({name, flags: []})),
|
||||
agents: makeParsedAgents(agentNames),
|
||||
mode,
|
||||
};
|
||||
},
|
||||
|
@ -47,7 +57,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
} = {}) => {
|
||||
return {
|
||||
type: 'connect',
|
||||
agents: agentNames.map((name) => ({name, flags: []})),
|
||||
agents: makeParsedAgents(agentNames),
|
||||
label,
|
||||
options: {
|
||||
line,
|
||||
|
@ -57,14 +67,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
};
|
||||
},
|
||||
|
||||
note: (agentNames, {
|
||||
type = 'note over',
|
||||
note: (type, agentNames, {
|
||||
mode = '',
|
||||
label = '',
|
||||
} = {}) => {
|
||||
return {
|
||||
type,
|
||||
agents: agentNames.map((name) => ({name, flags: []})),
|
||||
agents: makeParsedAgents(agentNames),
|
||||
mode,
|
||||
label,
|
||||
};
|
||||
|
@ -110,8 +119,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
};
|
||||
},
|
||||
|
||||
note: (agentNames, {
|
||||
type = jasmine.anything(),
|
||||
highlight: (agentNames, highlighted) => {
|
||||
return {
|
||||
type: 'agent highlight',
|
||||
agentNames,
|
||||
highlighted,
|
||||
};
|
||||
},
|
||||
|
||||
note: (type, agentNames, {
|
||||
mode = jasmine.anything(),
|
||||
label = jasmine.anything(),
|
||||
} = {}) => {
|
||||
|
@ -122,6 +138,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
label,
|
||||
};
|
||||
},
|
||||
|
||||
parallel: (stages) => {
|
||||
return {
|
||||
type: 'parallel',
|
||||
stages,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
describe('.generate', () => {
|
||||
|
@ -372,7 +395,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
|
||||
it('does not merge different modes of end', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.beginAgents(['C', 'D']),
|
||||
PARSED.beginAgents(['A', 'B', 'C', 'D']),
|
||||
PARSED.connect(['A', 'B']),
|
||||
PARSED.endAgents(['A', 'B', 'C']),
|
||||
]});
|
||||
|
@ -384,6 +407,86 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('adds parallel highlighting stages', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
|
||||
PARSED.connect(['A', {name: 'B', flags: ['stop']}]),
|
||||
]});
|
||||
expect(sequence.stages).toEqual([
|
||||
jasmine.anything(),
|
||||
GENERATED.parallel([
|
||||
GENERATED.highlight(['B'], true),
|
||||
GENERATED.connect(['A', 'B']),
|
||||
]),
|
||||
GENERATED.parallel([
|
||||
GENERATED.connect(['A', 'B']),
|
||||
GENERATED.highlight(['B'], false),
|
||||
]),
|
||||
jasmine.anything(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects conflicting flags', () => {
|
||||
expect(() => generator.generate({stages: [
|
||||
PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]),
|
||||
]})).toThrow();
|
||||
|
||||
expect(() => generator.generate({stages: [
|
||||
PARSED.connect([
|
||||
{name: 'A', flags: ['start']},
|
||||
{name: 'A', flags: ['stop']},
|
||||
]),
|
||||
]})).toThrow();
|
||||
});
|
||||
|
||||
it('adds implicit highlight end with implicit terminator', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
|
||||
]});
|
||||
expect(sequence.stages).toEqual([
|
||||
jasmine.anything(),
|
||||
jasmine.anything(),
|
||||
GENERATED.parallel([
|
||||
GENERATED.highlight(['B'], false),
|
||||
GENERATED.endAgents(['A', 'B']),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds implicit highlight end with explicit terminator', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
|
||||
PARSED.endAgents(['A', 'B']),
|
||||
]});
|
||||
expect(sequence.stages).toEqual([
|
||||
jasmine.anything(),
|
||||
jasmine.anything(),
|
||||
GENERATED.parallel([
|
||||
GENERATED.highlight(['B'], false),
|
||||
GENERATED.endAgents(['A', 'B']),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('collapses adjacent end statements containing highlighting', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.connect([
|
||||
{name: 'A', flags: ['start']},
|
||||
{name: 'B', flags: ['start']},
|
||||
]),
|
||||
PARSED.endAgents(['A']),
|
||||
PARSED.endAgents(['B']),
|
||||
]});
|
||||
expect(sequence.stages).toEqual([
|
||||
jasmine.anything(),
|
||||
jasmine.anything(),
|
||||
GENERATED.parallel([
|
||||
GENERATED.highlight(['A', 'B'], false),
|
||||
GENERATED.endAgents(['A', 'B']),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates virtual agents for block statements', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.blockBegin('if', 'abc'),
|
||||
|
@ -675,16 +778,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
|
||||
it('passes notes through', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.note(['A', 'B'], {
|
||||
type: 'note right',
|
||||
PARSED.note('note right', ['A', 'B'], {
|
||||
mode: 'foo',
|
||||
label: 'bar',
|
||||
}),
|
||||
]});
|
||||
expect(sequence.stages).toEqual([
|
||||
jasmine.anything(),
|
||||
GENERATED.note(['A', 'B'], {
|
||||
type: 'note right',
|
||||
GENERATED.note('note right', ['A', 'B'], {
|
||||
mode: 'foo',
|
||||
label: 'bar',
|
||||
}),
|
||||
|
@ -694,14 +795,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
|||
|
||||
it('defaults to showing notes around the entire diagram', () => {
|
||||
const sequence = generator.generate({stages: [
|
||||
PARSED.note([], {type: 'note right'}),
|
||||
PARSED.note([], {type: 'note left'}),
|
||||
PARSED.note([], {type: 'note over'}),
|
||||
PARSED.note('note right', []),
|
||||
PARSED.note('note left', []),
|
||||
PARSED.note('note over', []),
|
||||
]});
|
||||
expect(sequence.stages).toEqual([
|
||||
GENERATED.note([']'], {type: 'note right'}),
|
||||
GENERATED.note(['['], {type: 'note left'}),
|
||||
GENERATED.note(['[', ']'], {type: 'note over'}),
|
||||
GENERATED.note('note right', [']']),
|
||||
GENERATED.note('note left', ['[']),
|
||||
GENERATED.note('note over', ['[', ']']),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,218 @@
|
|||
define([
|
||||
'./BaseComponent',
|
||||
'core/ArrayUtilities',
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
BaseComponent,
|
||||
array,
|
||||
svg,
|
||||
SVGShapes
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
class CapBox {
|
||||
separation({label}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
|
||||
return {
|
||||
left: width / 2,
|
||||
right: width / 2,
|
||||
};
|
||||
}
|
||||
|
||||
topShift() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
render(y, {x, label}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const {height} = SVGShapes.renderBoxedText(label, {
|
||||
x,
|
||||
y,
|
||||
padding: config.padding,
|
||||
boxAttrs: config.boxAttrs,
|
||||
labelAttrs: config.labelAttrs,
|
||||
boxLayer: env.shapeLayer,
|
||||
labelLayer: env.labelLayer,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
return {
|
||||
lineTop: 0,
|
||||
lineBottom: height,
|
||||
height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CapCross {
|
||||
separation(agentInfo, env) {
|
||||
const config = env.theme.agentCap.cross;
|
||||
return {
|
||||
left: config.size / 2,
|
||||
right: config.size / 2,
|
||||
};
|
||||
}
|
||||
|
||||
topShift(agentInfo, env) {
|
||||
const config = env.theme.agentCap.cross;
|
||||
return config.size / 2;
|
||||
}
|
||||
|
||||
render(y, {x}, env) {
|
||||
const config = env.theme.agentCap.cross;
|
||||
const d = config.size / 2;
|
||||
|
||||
env.shapeLayer.appendChild(svg.make('path', Object.assign({
|
||||
'd': (
|
||||
'M ' + (x - d) + ' ' + y +
|
||||
' L ' + (x + d) + ' ' + (y + d * 2) +
|
||||
' M ' + (x + d) + ' ' + y +
|
||||
' L ' + (x - d) + ' ' + (y + d * 2)
|
||||
),
|
||||
}, config.attrs)));
|
||||
|
||||
return {
|
||||
lineTop: d,
|
||||
lineBottom: d,
|
||||
height: d * 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CapBar {
|
||||
separation({label}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
|
||||
return {
|
||||
left: width / 2,
|
||||
right: width / 2,
|
||||
};
|
||||
}
|
||||
|
||||
topShift(agentInfo, env) {
|
||||
const config = env.theme.agentCap.bar;
|
||||
return config.attrs.height / 2;
|
||||
}
|
||||
|
||||
render(y, {x, label}, env) {
|
||||
const configB = env.theme.agentCap.box;
|
||||
const config = env.theme.agentCap.bar;
|
||||
const width = (
|
||||
env.textSizer.measure(configB.labelAttrs, label).width +
|
||||
configB.padding.left +
|
||||
configB.padding.right
|
||||
);
|
||||
|
||||
env.shapeLayer.appendChild(svg.make('rect', Object.assign({
|
||||
'x': x - width / 2,
|
||||
'y': y,
|
||||
'width': width,
|
||||
}, config.attrs)));
|
||||
|
||||
return {
|
||||
lineTop: 0,
|
||||
lineBottom: config.attrs.height,
|
||||
height: config.attrs.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CapNone {
|
||||
separation({currentRad}) {
|
||||
return {left: currentRad, right: currentRad};
|
||||
}
|
||||
|
||||
topShift(agentInfo, env) {
|
||||
const config = env.theme.agentCap.none;
|
||||
return config.height;
|
||||
}
|
||||
|
||||
render(y, agentInfo, env) {
|
||||
const config = env.theme.agentCap.none;
|
||||
return {
|
||||
lineTop: config.height,
|
||||
lineBottom: 0,
|
||||
height: config.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const AGENT_CAPS = {
|
||||
'box': new CapBox(),
|
||||
'cross': new CapCross(),
|
||||
'bar': new CapBar(),
|
||||
'none': new CapNone(),
|
||||
};
|
||||
|
||||
class AgentCap extends BaseComponent {
|
||||
constructor(begin) {
|
||||
super();
|
||||
this.begin = begin;
|
||||
}
|
||||
|
||||
separation({mode, agentNames}, env) {
|
||||
if(this.begin) {
|
||||
array.mergeSets(env.visibleAgents, agentNames);
|
||||
} else {
|
||||
array.removeAll(env.visibleAgents, agentNames);
|
||||
}
|
||||
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
const separationFn = AGENT_CAPS[mode].separation;
|
||||
env.addSpacing(name, separationFn(agentInfo, env));
|
||||
});
|
||||
}
|
||||
|
||||
renderPre({mode, agentNames}, env) {
|
||||
let maxTopShift = 0;
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
const topShift = AGENT_CAPS[mode].topShift(agentInfo, env);
|
||||
maxTopShift = Math.max(maxTopShift, topShift);
|
||||
});
|
||||
return {
|
||||
agentNames,
|
||||
topShift: maxTopShift,
|
||||
};
|
||||
}
|
||||
|
||||
render({mode, agentNames}, env) {
|
||||
let maxEnd = 0;
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
const topShift = AGENT_CAPS[mode].topShift(agentInfo, env);
|
||||
const y0 = env.primaryY - topShift;
|
||||
const shifts = AGENT_CAPS[mode].render(
|
||||
y0,
|
||||
agentInfo,
|
||||
env
|
||||
);
|
||||
maxEnd = Math.max(maxEnd, y0 + shifts.height);
|
||||
if(this.begin) {
|
||||
env.drawAgentLine(name, y0 + shifts.lineBottom);
|
||||
} else {
|
||||
env.drawAgentLine(name, y0 + shifts.lineTop, true);
|
||||
}
|
||||
});
|
||||
return maxEnd + env.theme.actionMargin;
|
||||
}
|
||||
}
|
||||
|
||||
BaseComponent.register('agent begin', new AgentCap(true));
|
||||
BaseComponent.register('agent end', new AgentCap(false));
|
||||
|
||||
return AgentCap;
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
defineDescribe('AgentCap', [
|
||||
'./AgentCap',
|
||||
'./BaseComponent',
|
||||
], (
|
||||
AgentCap,
|
||||
BaseComponent
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('agent begin')).toEqual(jasmine.any(AgentCap));
|
||||
expect(components.get('agent end')).toEqual(jasmine.any(AgentCap));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
define(['./BaseComponent'], (BaseComponent) => {
|
||||
'use strict';
|
||||
|
||||
class AgentHighlight extends BaseComponent {
|
||||
separationPre({agentNames, highlighted}, env) {
|
||||
const rad = highlighted ? env.theme.agentLineHighlightRadius : 0;
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
const maxRad = Math.max(agentInfo.currentMaxRad, rad);
|
||||
agentInfo.currentRad = rad;
|
||||
agentInfo.currentMaxRad = maxRad;
|
||||
});
|
||||
}
|
||||
|
||||
render({agentNames, highlighted}, env) {
|
||||
const rad = highlighted ? env.theme.agentLineHighlightRadius : 0;
|
||||
agentNames.forEach((name) => {
|
||||
env.drawAgentLine(name, env.primaryY);
|
||||
env.agentInfos.get(name).currentRad = rad;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BaseComponent.register('agent highlight', new AgentHighlight());
|
||||
|
||||
return AgentHighlight;
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
defineDescribe('AgentHighlight', [
|
||||
'./AgentHighlight',
|
||||
'./BaseComponent',
|
||||
], (
|
||||
AgentHighlight,
|
||||
BaseComponent
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const highlight = new AgentHighlight();
|
||||
|
||||
const theme = {
|
||||
agentLineHighlightRadius: 2,
|
||||
};
|
||||
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('agent highlight')).toEqual(
|
||||
jasmine.any(AgentHighlight)
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the radius of the agent lines when checking separation', () => {
|
||||
const agentInfo = {currentRad: 0, currentMaxRad: 1};
|
||||
const agentInfos = new Map();
|
||||
agentInfos.set('foo', agentInfo);
|
||||
const env = {
|
||||
theme,
|
||||
agentInfos,
|
||||
};
|
||||
highlight.separationPre({agentNames: ['foo'], highlighted: true}, env);
|
||||
expect(agentInfo.currentRad).toEqual(2);
|
||||
expect(agentInfo.currentMaxRad).toEqual(2);
|
||||
});
|
||||
|
||||
it('keeps the largest maximum radius', () => {
|
||||
const agentInfo = {currentRad: 0, currentMaxRad: 3};
|
||||
const agentInfos = new Map();
|
||||
agentInfos.set('foo', agentInfo);
|
||||
const env = {
|
||||
theme,
|
||||
agentInfos,
|
||||
};
|
||||
highlight.separationPre({agentNames: ['foo'], highlighted: true}, env);
|
||||
expect(agentInfo.currentRad).toEqual(2);
|
||||
expect(agentInfo.currentMaxRad).toEqual(3);
|
||||
});
|
||||
|
||||
it('sets the radius to 0 when highlighting is disabled', () => {
|
||||
const agentInfo = {currentRad: 0, currentMaxRad: 1};
|
||||
const agentInfos = new Map();
|
||||
agentInfos.set('foo', agentInfo);
|
||||
const env = {
|
||||
theme,
|
||||
agentInfos,
|
||||
};
|
||||
highlight.separationPre({agentNames: ['foo'], highlighted: false}, env);
|
||||
expect(agentInfo.currentRad).toEqual(0);
|
||||
expect(agentInfo.currentMaxRad).toEqual(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
define(() => {
|
||||
'use strict';
|
||||
|
||||
class BaseComponent {
|
||||
makeState(/*state*/) {
|
||||
}
|
||||
|
||||
resetState(state) {
|
||||
this.makeState(state);
|
||||
}
|
||||
|
||||
separationPre(/*stage, {
|
||||
theme,
|
||||
agentInfos,
|
||||
visibleAgents,
|
||||
textSizer,
|
||||
addSpacing,
|
||||
addSeparation,
|
||||
}*/) {
|
||||
}
|
||||
|
||||
separation(/*stage, {
|
||||
theme,
|
||||
agentInfos,
|
||||
visibleAgents,
|
||||
textSizer,
|
||||
addSpacing,
|
||||
addSeparation,
|
||||
}*/) {
|
||||
}
|
||||
|
||||
renderPre(/*stage, {
|
||||
theme,
|
||||
agentInfos,
|
||||
textSizer,
|
||||
state,
|
||||
}*/) {
|
||||
return {};
|
||||
}
|
||||
|
||||
render(/*stage, {
|
||||
topY,
|
||||
primaryY,
|
||||
shapeLayer,
|
||||
labelLayer,
|
||||
theme,
|
||||
agentInfos,
|
||||
textSizer,
|
||||
SVGTextBlockClass,
|
||||
state,
|
||||
}*/) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const components = new Map();
|
||||
|
||||
BaseComponent.register = (name, component) => {
|
||||
components.set(name, component);
|
||||
};
|
||||
|
||||
BaseComponent.getComponents = () => {
|
||||
return components;
|
||||
};
|
||||
|
||||
return BaseComponent;
|
||||
});
|
|
@ -0,0 +1,234 @@
|
|||
define([
|
||||
'./BaseComponent',
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
BaseComponent,
|
||||
svg,
|
||||
SVGShapes
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) {
|
||||
container.appendChild(svg.make(
|
||||
attrs.fill === 'none' ? 'polyline' : 'polygon',
|
||||
Object.assign({
|
||||
'points': (
|
||||
(x + dx) + ' ' + (y - dy) + ' ' +
|
||||
x + ' ' + y + ' ' +
|
||||
(x + dx) + ' ' + (y + dy)
|
||||
),
|
||||
}, attrs)
|
||||
));
|
||||
}
|
||||
|
||||
function getArrowShort(theme) {
|
||||
const arrow = theme.connect.arrow;
|
||||
const h = arrow.height / 2;
|
||||
const w = arrow.width;
|
||||
const t = arrow.attrs['stroke-width'] * 0.5;
|
||||
const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5;
|
||||
const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1);
|
||||
return lineStroke + arrowDistance;
|
||||
}
|
||||
|
||||
class Connect extends BaseComponent {
|
||||
separation({agentNames, label}, env) {
|
||||
const config = env.theme.connect;
|
||||
|
||||
const labelWidth = (
|
||||
env.textSizer.measure(config.label.attrs, label).width +
|
||||
config.label.padding * 2
|
||||
);
|
||||
|
||||
const short = getArrowShort(env.theme);
|
||||
|
||||
const info1 = env.agentInfos.get(agentNames[0]);
|
||||
if(agentNames[0] === agentNames[1]) {
|
||||
env.addSpacing(agentNames[0], {
|
||||
left: 0,
|
||||
right: (
|
||||
info1.currentMaxRad +
|
||||
labelWidth +
|
||||
config.arrow.width +
|
||||
short +
|
||||
config.loopbackRadius
|
||||
),
|
||||
});
|
||||
} else {
|
||||
const info2 = env.agentInfos.get(agentNames[1]);
|
||||
env.addSeparation(
|
||||
agentNames[0],
|
||||
agentNames[1],
|
||||
|
||||
info1.currentMaxRad +
|
||||
info2.currentMaxRad +
|
||||
labelWidth +
|
||||
config.arrow.width * 2 +
|
||||
short * 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderSelfConnect({label, agentNames, options}, env) {
|
||||
const config = env.theme.connect;
|
||||
const from = env.agentInfos.get(agentNames[0]);
|
||||
|
||||
const dy = config.arrow.height / 2;
|
||||
const short = getArrowShort(env.theme);
|
||||
|
||||
const height = (
|
||||
env.textSizer.measureHeight(config.label.attrs, label) +
|
||||
config.label.margin.top +
|
||||
config.label.margin.bottom
|
||||
);
|
||||
|
||||
const lineX = from.x + from.currentRad;
|
||||
const y0 = env.primaryY;
|
||||
const x0 = (
|
||||
lineX +
|
||||
short +
|
||||
config.arrow.width +
|
||||
config.label.padding
|
||||
);
|
||||
|
||||
const renderedText = SVGShapes.renderBoxedText(label, {
|
||||
x: x0 - config.mask.padding.left,
|
||||
y: y0 - height + config.label.margin.top,
|
||||
padding: config.mask.padding,
|
||||
boxAttrs: config.mask.maskAttrs,
|
||||
labelAttrs: config.label.loopbackAttrs,
|
||||
boxLayer: env.maskLayer,
|
||||
labelLayer: env.labelLayer,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
});
|
||||
const r = config.loopbackRadius;
|
||||
const x1 = (
|
||||
x0 +
|
||||
renderedText.width +
|
||||
config.label.padding -
|
||||
config.mask.padding.left -
|
||||
config.mask.padding.right
|
||||
);
|
||||
const y1 = y0 + r * 2;
|
||||
|
||||
env.shapeLayer.appendChild(svg.make('path', Object.assign({
|
||||
'd': (
|
||||
'M ' + (lineX + (options.left ? short : 0)) + ' ' + y0 +
|
||||
' L ' + x1 + ' ' + y0 +
|
||||
' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 +
|
||||
' L ' + (lineX + (options.right ? short : 0)) + ' ' + y1
|
||||
),
|
||||
}, config.lineAttrs[options.line])));
|
||||
|
||||
if(options.left) {
|
||||
drawHorizontalArrowHead(env.shapeLayer, {
|
||||
x: lineX + short,
|
||||
y: y0,
|
||||
dx: config.arrow.width,
|
||||
dy,
|
||||
attrs: config.arrow.attrs,
|
||||
});
|
||||
}
|
||||
|
||||
if(options.right) {
|
||||
drawHorizontalArrowHead(env.shapeLayer, {
|
||||
x: lineX + short,
|
||||
y: y1,
|
||||
dx: config.arrow.width,
|
||||
dy,
|
||||
attrs: config.arrow.attrs,
|
||||
});
|
||||
}
|
||||
|
||||
return y1 + dy + env.theme.actionMargin;
|
||||
}
|
||||
|
||||
renderSimpleConnect({label, agentNames, options}, env) {
|
||||
const config = env.theme.connect;
|
||||
const from = env.agentInfos.get(agentNames[0]);
|
||||
const to = env.agentInfos.get(agentNames[1]);
|
||||
|
||||
const dy = config.arrow.height / 2;
|
||||
const dir = (from.x < to.x) ? 1 : -1;
|
||||
const short = getArrowShort(env.theme);
|
||||
|
||||
const height = (
|
||||
env.textSizer.measureHeight(config.label.attrs, label) +
|
||||
config.label.margin.top +
|
||||
config.label.margin.bottom
|
||||
);
|
||||
|
||||
const x0 = from.x + from.currentRad * dir;
|
||||
const x1 = to.x - to.currentRad * dir;
|
||||
let y = env.primaryY;
|
||||
|
||||
SVGShapes.renderBoxedText(label, {
|
||||
x: (x0 + x1) / 2,
|
||||
y: y - height + config.label.margin.top,
|
||||
padding: config.mask.padding,
|
||||
boxAttrs: config.mask.maskAttrs,
|
||||
labelAttrs: config.label.attrs,
|
||||
boxLayer: env.maskLayer,
|
||||
labelLayer: env.labelLayer,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
env.shapeLayer.appendChild(svg.make('line', Object.assign({
|
||||
'x1': x0 + (options.left ? short : 0) * dir,
|
||||
'y1': y,
|
||||
'x2': x1 - (options.right ? short : 0) * dir,
|
||||
'y2': y,
|
||||
}, config.lineAttrs[options.line])));
|
||||
|
||||
if(options.left) {
|
||||
drawHorizontalArrowHead(env.shapeLayer, {
|
||||
x: x0 + short * dir,
|
||||
y,
|
||||
dx: config.arrow.width * dir,
|
||||
dy,
|
||||
attrs: config.arrow.attrs,
|
||||
});
|
||||
}
|
||||
|
||||
if(options.right) {
|
||||
drawHorizontalArrowHead(env.shapeLayer, {
|
||||
x: x1 - short * dir,
|
||||
y,
|
||||
dx: -config.arrow.width * dir,
|
||||
dy,
|
||||
attrs: config.arrow.attrs,
|
||||
});
|
||||
}
|
||||
|
||||
return y + dy + env.theme.actionMargin;
|
||||
}
|
||||
|
||||
renderPre({label, agentNames}, env) {
|
||||
const config = env.theme.connect;
|
||||
|
||||
const height = (
|
||||
env.textSizer.measureHeight(config.label.attrs, label) +
|
||||
config.label.margin.top +
|
||||
config.label.margin.bottom
|
||||
);
|
||||
|
||||
return {
|
||||
agentNames,
|
||||
topShift: Math.max(config.arrow.height / 2, height),
|
||||
};
|
||||
}
|
||||
|
||||
render(stage, env) {
|
||||
if(stage.agentNames[0] === stage.agentNames[1]) {
|
||||
return this.renderSelfConnect(stage, env);
|
||||
} else {
|
||||
return this.renderSimpleConnect(stage, env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BaseComponent.register('connect', new Connect());
|
||||
|
||||
return Connect;
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
defineDescribe('Connect', [
|
||||
'./Connect',
|
||||
'./BaseComponent',
|
||||
], (
|
||||
Connect,
|
||||
BaseComponent
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('connect')).toEqual(jasmine.any(Connect));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
define(['./BaseComponent'], (BaseComponent) => {
|
||||
'use strict';
|
||||
|
||||
class Mark extends BaseComponent {
|
||||
makeState(state) {
|
||||
state.marks = new Map();
|
||||
}
|
||||
|
||||
resetState(state) {
|
||||
state.marks.clear();
|
||||
}
|
||||
|
||||
render({name}, {topY, state}) {
|
||||
state.marks.set(name, topY);
|
||||
}
|
||||
}
|
||||
|
||||
class Async extends BaseComponent {
|
||||
renderPre({target}, {state}) {
|
||||
let y = 0;
|
||||
if(target && state.marks) {
|
||||
y = state.marks.get(target) || 0;
|
||||
}
|
||||
return {
|
||||
asynchronousY: y,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
BaseComponent.register('mark', new Mark());
|
||||
BaseComponent.register('async', new Async());
|
||||
|
||||
return {
|
||||
Mark,
|
||||
Async,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
defineDescribe('Marker', [
|
||||
'./Marker',
|
||||
'./BaseComponent',
|
||||
], (
|
||||
Marker,
|
||||
BaseComponent
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const mark = new Marker.Mark();
|
||||
const async = new Marker.Async();
|
||||
|
||||
describe('Mark', () => {
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('mark')).toEqual(jasmine.any(Marker.Mark));
|
||||
});
|
||||
|
||||
it('records y coordinates when rendered', () => {
|
||||
const state = {};
|
||||
mark.makeState(state);
|
||||
mark.render({name: 'foo'}, {topY: 7, state});
|
||||
expect(state.marks.get('foo')).toEqual(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Async', () => {
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('async')).toEqual(jasmine.any(Marker.Async));
|
||||
});
|
||||
|
||||
it('retrieves y coordinates when rendered', () => {
|
||||
const state = {};
|
||||
mark.makeState(state);
|
||||
mark.render({name: 'foo'}, {topY: 7, state});
|
||||
const result = async.renderPre({target: 'foo'}, {state});
|
||||
expect(result.asynchronousY).toEqual(7);
|
||||
});
|
||||
|
||||
it('returns 0 if no target is given', () => {
|
||||
const state = {};
|
||||
mark.makeState(state);
|
||||
mark.render({name: 'foo'}, {topY: 7, state});
|
||||
const result = async.renderPre({target: ''}, {state});
|
||||
expect(result.asynchronousY).toEqual(0);
|
||||
});
|
||||
|
||||
it('falls-back to 0 if the target is not found', () => {
|
||||
const state = {};
|
||||
mark.makeState(state);
|
||||
mark.render({name: 'foo'}, {topY: 7, state});
|
||||
const result = async.renderPre({target: 'bar'}, {state});
|
||||
expect(result.asynchronousY).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,258 @@
|
|||
define(['./BaseComponent'], (BaseComponent) => {
|
||||
'use strict';
|
||||
|
||||
function findExtremes(agentInfos, agentNames) {
|
||||
let min = null;
|
||||
let max = null;
|
||||
agentNames.forEach((name) => {
|
||||
const info = agentInfos.get(name);
|
||||
if(min === null || info.index < min.index) {
|
||||
min = info;
|
||||
}
|
||||
if(max === null || info.index > max.index) {
|
||||
max = info;
|
||||
}
|
||||
});
|
||||
return {
|
||||
left: min.label,
|
||||
right: max.label,
|
||||
};
|
||||
}
|
||||
|
||||
class NoteComponent extends BaseComponent {
|
||||
renderPre({agentNames}) {
|
||||
return {agentNames};
|
||||
}
|
||||
|
||||
renderNote({
|
||||
xMid = null,
|
||||
x0 = null,
|
||||
x1 = null,
|
||||
anchor,
|
||||
mode,
|
||||
label,
|
||||
}, env) {
|
||||
const config = env.theme.note[mode];
|
||||
|
||||
const y = env.topY + config.margin.top + config.padding.top;
|
||||
const labelNode = new env.SVGTextBlockClass(env.labelLayer, {
|
||||
attrs: config.labelAttrs,
|
||||
text: label,
|
||||
y,
|
||||
});
|
||||
|
||||
const fullW = (
|
||||
labelNode.width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
const fullH = (
|
||||
config.padding.top +
|
||||
labelNode.height +
|
||||
config.padding.bottom
|
||||
);
|
||||
if(x0 === null && xMid !== null) {
|
||||
x0 = xMid - fullW / 2;
|
||||
}
|
||||
if(x1 === null && x0 !== null) {
|
||||
x1 = x0 + fullW;
|
||||
} else if(x0 === null) {
|
||||
x0 = x1 - fullW;
|
||||
}
|
||||
switch(config.labelAttrs['text-anchor']) {
|
||||
case 'middle':
|
||||
labelNode.set({
|
||||
x: (
|
||||
x0 + config.padding.left +
|
||||
x1 - config.padding.right
|
||||
) / 2,
|
||||
y,
|
||||
});
|
||||
break;
|
||||
case 'end':
|
||||
labelNode.set({x: x1 - config.padding.right, y});
|
||||
break;
|
||||
default:
|
||||
labelNode.set({x: x0 + config.padding.left, y});
|
||||
break;
|
||||
}
|
||||
|
||||
env.shapeLayer.appendChild(config.boxRenderer({
|
||||
x: x0,
|
||||
y: env.topY + config.margin.top,
|
||||
width: x1 - x0,
|
||||
height: fullH,
|
||||
}));
|
||||
|
||||
return (
|
||||
env.topY +
|
||||
config.margin.top +
|
||||
fullH +
|
||||
config.margin.bottom +
|
||||
env.theme.actionMargin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoteOver extends NoteComponent {
|
||||
separation({agentNames, mode, label}, env) {
|
||||
const config = env.theme.note[mode];
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
|
||||
if(agentNames.length > 1) {
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
|
||||
const hangL = infoL.currentMaxRad + config.overlap.left;
|
||||
const hangR = infoR.currentMaxRad + config.overlap.right;
|
||||
|
||||
env.addSeparation(left, right, width - hangL - hangR);
|
||||
|
||||
env.addSpacing(left, {left: hangL, right: 0});
|
||||
env.addSpacing(right, {left: 0, right: hangR});
|
||||
} else {
|
||||
env.addSpacing(agentNames[0], {
|
||||
left: width / 2,
|
||||
right: width / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render({agentNames, mode, label}, env) {
|
||||
const config = env.theme.note[mode];
|
||||
|
||||
if(agentNames.length > 1) {
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
return this.renderNote({
|
||||
x0: infoL.x - infoL.currentRad - config.overlap.left,
|
||||
x1: infoR.x + infoR.currentRad + config.overlap.right,
|
||||
anchor: 'middle',
|
||||
mode,
|
||||
label,
|
||||
}, env);
|
||||
} else {
|
||||
const xMid = env.agentInfos.get(agentNames[0]).x;
|
||||
return this.renderNote({
|
||||
xMid,
|
||||
anchor: 'middle',
|
||||
mode,
|
||||
label,
|
||||
}, env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoteSide extends NoteComponent {
|
||||
constructor(isRight) {
|
||||
super();
|
||||
this.isRight = isRight;
|
||||
}
|
||||
|
||||
separation({agentNames, mode, label}, env) {
|
||||
const config = env.theme.note[mode];
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
config.padding.left +
|
||||
config.padding.right +
|
||||
config.margin.left +
|
||||
config.margin.right
|
||||
);
|
||||
|
||||
if(this.isRight) {
|
||||
const info = env.agentInfos.get(right);
|
||||
env.addSpacing(right, {
|
||||
left: 0,
|
||||
right: width + info.currentMaxRad,
|
||||
});
|
||||
} else {
|
||||
const info = env.agentInfos.get(left);
|
||||
env.addSpacing(left, {
|
||||
left: width + info.currentMaxRad,
|
||||
right: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render({agentNames, mode, label}, env) {
|
||||
const config = env.theme.note[mode];
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
if(this.isRight) {
|
||||
const info = env.agentInfos.get(right);
|
||||
const x0 = info.x + info.currentRad + config.margin.left;
|
||||
return this.renderNote({
|
||||
x0,
|
||||
anchor: 'start',
|
||||
mode,
|
||||
label,
|
||||
}, env);
|
||||
} else {
|
||||
const info = env.agentInfos.get(left);
|
||||
const x1 = info.x - info.currentRad - config.margin.right;
|
||||
return this.renderNote({
|
||||
x1,
|
||||
anchor: 'end',
|
||||
mode,
|
||||
label,
|
||||
}, env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoteBetween extends NoteComponent {
|
||||
separation({agentNames, mode, label}, env) {
|
||||
const config = env.theme.note[mode];
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
|
||||
env.addSeparation(
|
||||
left,
|
||||
right,
|
||||
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
config.padding.left +
|
||||
config.padding.right +
|
||||
config.margin.left +
|
||||
config.margin.right +
|
||||
infoL.currentMaxRad +
|
||||
infoR.currentMaxRad
|
||||
);
|
||||
}
|
||||
|
||||
render({agentNames, mode, label}, env) {
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
const xMid = (
|
||||
infoL.x + infoL.currentRad +
|
||||
infoR.x - infoR.currentRad
|
||||
) / 2;
|
||||
|
||||
return this.renderNote({
|
||||
xMid,
|
||||
anchor: 'middle',
|
||||
mode,
|
||||
label,
|
||||
}, env);
|
||||
}
|
||||
}
|
||||
|
||||
NoteComponent.NoteOver = NoteOver;
|
||||
NoteComponent.NoteSide = NoteSide;
|
||||
NoteComponent.NoteBetween = NoteBetween;
|
||||
|
||||
BaseComponent.register('note over', new NoteOver());
|
||||
BaseComponent.register('note left', new NoteSide(false));
|
||||
BaseComponent.register('note right', new NoteSide(true));
|
||||
BaseComponent.register('note between', new NoteBetween());
|
||||
|
||||
return NoteComponent;
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
defineDescribe('Note', [
|
||||
'./Note',
|
||||
'./BaseComponent',
|
||||
], (
|
||||
Note,
|
||||
BaseComponent
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
describe('NoteOver', () => {
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('note over')).toEqual(
|
||||
jasmine.any(Note.NoteOver)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NoteSide', () => {
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('note left')).toEqual(
|
||||
jasmine.any(Note.NoteSide)
|
||||
);
|
||||
expect(components.get('note right')).toEqual(
|
||||
jasmine.any(Note.NoteSide)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NoteBetween', () => {
|
||||
it('registers itself with the component store', () => {
|
||||
const components = BaseComponent.getComponents();
|
||||
expect(components.get('note between')).toEqual(
|
||||
jasmine.any(Note.NoteBetween)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,14 +1,4 @@
|
|||
define([
|
||||
'core/ArrayUtilities',
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGTextBlock',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
array,
|
||||
svg,
|
||||
SVGTextBlock,
|
||||
SVGShapes
|
||||
) => {
|
||||
define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
|
||||
'use strict';
|
||||
|
||||
const LINE_HEIGHT = 1.3;
|
||||
|
@ -18,6 +8,7 @@ define([
|
|||
outerMargin: 5,
|
||||
agentMargin: 10,
|
||||
actionMargin: 5,
|
||||
agentLineHighlightRadius: 4,
|
||||
|
||||
agentCap: {
|
||||
box: {
|
||||
|
|
|
@ -9,5 +9,10 @@ define([
|
|||
'sequence/Generator_spec',
|
||||
'sequence/Renderer_spec',
|
||||
'sequence/themes/Basic_spec',
|
||||
'sequence/components/AgentCap_spec',
|
||||
'sequence/components/AgentHighlight_spec',
|
||||
'sequence/components/Connect_spec',
|
||||
'sequence/components/Marker_spec',
|
||||
'sequence/components/Note_spec',
|
||||
'sequence/sequence_integration_spec',
|
||||
]);
|
||||
|
|
|
@ -106,5 +106,6 @@ define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => {
|
|||
renderBox,
|
||||
renderNote,
|
||||
renderBoxedText,
|
||||
TextBlock: SVGTextBlock,
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue