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 -> 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
|
# Arrows leaving on the left and right of the diagram
|
||||||
[ -> Foo: From the left
|
[ -> Foo: From the left
|
||||||
[ <- Foo: To 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) {
|
function removeAll(target, b = null, equalityCheck = null) {
|
||||||
if(!b) {
|
if(!b) {
|
||||||
return;
|
return;
|
||||||
|
@ -50,6 +59,7 @@ define(() => {
|
||||||
return {
|
return {
|
||||||
indexOf,
|
indexOf,
|
||||||
mergeSets,
|
mergeSets,
|
||||||
|
hasIntersection,
|
||||||
removeAll,
|
removeAll,
|
||||||
remove,
|
remove,
|
||||||
last,
|
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', () => {
|
describe('.removeAll', () => {
|
||||||
it('removes elements from the first array', () => {
|
it('removes elements from the first array', () => {
|
||||||
const p1 = ['a', 'b', 'c'];
|
const p1 = ['a', 'b', 'c'];
|
||||||
|
|
|
@ -4,6 +4,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
class AgentState {
|
class AgentState {
|
||||||
constructor(visible, locked = false) {
|
constructor(visible, locked = false) {
|
||||||
this.visible = visible;
|
this.visible = visible;
|
||||||
|
this.highlighted = false;
|
||||||
this.locked = locked;
|
this.locked = locked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,13 +25,157 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
return agent.name;
|
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 LOCKED_AGENT = new AgentState(false, true);
|
||||||
const DEFAULT_AGENT = new AgentState(false);
|
const DEFAULT_AGENT = new AgentState(false);
|
||||||
|
|
||||||
const NOTE_DEFAULT_AGENTS = {
|
const NOTE_DEFAULT_AGENTS = {
|
||||||
'note over': [{name: '['}, {name: ']'}],
|
'note over': [{name: '[', flags: []}, {name: ']', flags: []}],
|
||||||
'note left': [{name: '['}],
|
'note left': [{name: '[', flags: []}],
|
||||||
'note right': [{name: ']'}],
|
'note right': [{name: ']', flags: []}],
|
||||||
};
|
};
|
||||||
|
|
||||||
return class Generator {
|
return class Generator {
|
||||||
|
@ -61,32 +206,30 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
this.handleStage = this.handleStage.bind(this);
|
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) {
|
addStage(stage, isVisible = true) {
|
||||||
|
if(!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.currentSection.stages.push(stage);
|
this.currentSection.stages.push(stage);
|
||||||
if(isVisible) {
|
if(isVisible) {
|
||||||
this.currentNest.hasContent = true;
|
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) {
|
defineAgents(agents) {
|
||||||
array.mergeSets(this.currentNest.agents, agents, agentEqCheck);
|
array.mergeSets(this.currentNest.agents, agents, agentEqCheck);
|
||||||
array.mergeSets(this.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;
|
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
|
||||||
if(state.locked) {
|
if(state.locked) {
|
||||||
if(checked) {
|
if(checked) {
|
||||||
throw new Error('Cannot begin/end agent: ' + agent);
|
throw new Error(
|
||||||
|
'Cannot begin/end agent: ' + agent.name
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +250,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
return state.visible !== visible;
|
return state.visible !== visible;
|
||||||
});
|
});
|
||||||
if(filteredAgents.length === 0) {
|
if(filteredAgents.length === 0) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
filteredAgents.forEach((agent) => {
|
filteredAgents.forEach((agent) => {
|
||||||
const state = this.agentStates.get(agent.name);
|
const state = this.agentStates.get(agent.name);
|
||||||
|
@ -115,19 +260,42 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
this.agentStates.set(agent.name, new AgentState(visible));
|
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);
|
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) {
|
beginNested(mode, label, name) {
|
||||||
|
@ -173,15 +341,26 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
|
|
||||||
handleConnect({agents, label, options}) {
|
handleConnect({agents, label, options}) {
|
||||||
const colAgents = agents.map(convertAgent);
|
const colAgents = agents.map(convertAgent);
|
||||||
this.setAgentVis(colAgents, true, 'box');
|
this.addStage(this.setAgentVis(colAgents, true, 'box'));
|
||||||
this.defineAgents(colAgents);
|
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',
|
type: 'connect',
|
||||||
agentNames: agents.map(getAgentName),
|
agentNames: agents.map(getAgentName),
|
||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.addParallelStages([
|
||||||
|
this.setAgentHighlight(startAgents, true, true),
|
||||||
|
connectStage,
|
||||||
|
this.setAgentHighlight(stopAgents, false, true),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNote({type, agents, mode, label}) {
|
handleNote({type, agents, mode, label}) {
|
||||||
|
@ -192,7 +371,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
colAgents = agents.map(convertAgent);
|
colAgents = agents.map(convertAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setAgentVis(colAgents, true, 'box');
|
this.addStage(this.setAgentVis(colAgents, true, 'box'));
|
||||||
this.defineAgents(colAgents);
|
this.defineAgents(colAgents);
|
||||||
|
|
||||||
this.addStage({
|
this.addStage({
|
||||||
|
@ -209,11 +388,19 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAgentBegin({agents, mode}) {
|
handleAgentBegin({agents, mode}) {
|
||||||
this.setAgentVis(agents.map(convertAgent), true, mode, true);
|
this.addStage(this.setAgentVis(
|
||||||
|
agents.map(convertAgent),
|
||||||
|
true,
|
||||||
|
mode,
|
||||||
|
true
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAgentEnd({agents, mode}) {
|
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}) {
|
handleBlockBegin({mode, label}) {
|
||||||
|
@ -230,6 +417,7 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
containerMode + ')'
|
containerMode + ')'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
optimiseStages(this.currentSection.stages);
|
||||||
this.currentSection = {
|
this.currentSection = {
|
||||||
mode,
|
mode,
|
||||||
label,
|
label,
|
||||||
|
@ -242,12 +430,13 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
if(this.nesting.length <= 1) {
|
if(this.nesting.length <= 1) {
|
||||||
throw new Error('Invalid block nesting (too many "end"s)');
|
throw new Error('Invalid block nesting (too many "end"s)');
|
||||||
}
|
}
|
||||||
|
optimiseStages(this.currentSection.stages);
|
||||||
const nested = this.nesting.pop();
|
const nested = this.nesting.pop();
|
||||||
this.currentNest = array.last(this.nesting);
|
this.currentNest = array.last(this.nesting);
|
||||||
this.currentSection = array.last(this.currentNest.stage.sections);
|
this.currentSection = array.last(this.currentNest.stage.sections);
|
||||||
if(nested.hasContent) {
|
if(nested.hasContent) {
|
||||||
this.defineAgents(nested.agents);
|
this.defineAgents(nested.agents);
|
||||||
this.addBounds(
|
addBounds(
|
||||||
this.agents,
|
this.agents,
|
||||||
nested.leftAgent,
|
nested.leftAgent,
|
||||||
nested.rightAgent,
|
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.agents,
|
||||||
this.currentNest.leftAgent,
|
this.currentNest.leftAgent,
|
||||||
this.currentNest.rightAgent
|
this.currentNest.rightAgent
|
||||||
);
|
);
|
||||||
|
optimiseStages(globals.stages);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -296,4 +491,3 @@ define(['core/ArrayUtilities'], (array) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
|
|
||||||
const generator = new 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 = {
|
const PARSED = {
|
||||||
blockBegin: (mode, label) => {
|
blockBegin: (mode, label) => {
|
||||||
return {type: 'block begin', mode, label};
|
return {type: 'block begin', mode, label};
|
||||||
|
@ -19,14 +29,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
defineAgents: (agentNames) => {
|
defineAgents: (agentNames) => {
|
||||||
return {
|
return {
|
||||||
type: 'agent define',
|
type: 'agent define',
|
||||||
agents: agentNames.map((name) => ({name, flags: []})),
|
agents: makeParsedAgents(agentNames),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
beginAgents: (agentNames, {mode = 'box'} = {}) => {
|
beginAgents: (agentNames, {mode = 'box'} = {}) => {
|
||||||
return {
|
return {
|
||||||
type: 'agent begin',
|
type: 'agent begin',
|
||||||
agents: agentNames.map((name) => ({name, flags: []})),
|
agents: makeParsedAgents(agentNames),
|
||||||
mode,
|
mode,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -34,7 +44,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
endAgents: (agentNames, {mode = 'cross'} = {}) => {
|
endAgents: (agentNames, {mode = 'cross'} = {}) => {
|
||||||
return {
|
return {
|
||||||
type: 'agent end',
|
type: 'agent end',
|
||||||
agents: agentNames.map((name) => ({name, flags: []})),
|
agents: makeParsedAgents(agentNames),
|
||||||
mode,
|
mode,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -47,7 +57,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
return {
|
return {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
agents: agentNames.map((name) => ({name, flags: []})),
|
agents: makeParsedAgents(agentNames),
|
||||||
label,
|
label,
|
||||||
options: {
|
options: {
|
||||||
line,
|
line,
|
||||||
|
@ -57,14 +67,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
note: (agentNames, {
|
note: (type, agentNames, {
|
||||||
type = 'note over',
|
|
||||||
mode = '',
|
mode = '',
|
||||||
label = '',
|
label = '',
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
agents: agentNames.map((name) => ({name, flags: []})),
|
agents: makeParsedAgents(agentNames),
|
||||||
mode,
|
mode,
|
||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
|
@ -110,8 +119,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
note: (agentNames, {
|
highlight: (agentNames, highlighted) => {
|
||||||
type = jasmine.anything(),
|
return {
|
||||||
|
type: 'agent highlight',
|
||||||
|
agentNames,
|
||||||
|
highlighted,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
note: (type, agentNames, {
|
||||||
mode = jasmine.anything(),
|
mode = jasmine.anything(),
|
||||||
label = jasmine.anything(),
|
label = jasmine.anything(),
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
|
@ -122,6 +138,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parallel: (stages) => {
|
||||||
|
return {
|
||||||
|
type: 'parallel',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('.generate', () => {
|
describe('.generate', () => {
|
||||||
|
@ -372,7 +395,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
|
|
||||||
it('does not merge different modes of end', () => {
|
it('does not merge different modes of end', () => {
|
||||||
const sequence = generator.generate({stages: [
|
const sequence = generator.generate({stages: [
|
||||||
PARSED.beginAgents(['C', 'D']),
|
PARSED.beginAgents(['A', 'B', 'C', 'D']),
|
||||||
PARSED.connect(['A', 'B']),
|
PARSED.connect(['A', 'B']),
|
||||||
PARSED.endAgents(['A', 'B', 'C']),
|
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', () => {
|
it('creates virtual agents for block statements', () => {
|
||||||
const sequence = generator.generate({stages: [
|
const sequence = generator.generate({stages: [
|
||||||
PARSED.blockBegin('if', 'abc'),
|
PARSED.blockBegin('if', 'abc'),
|
||||||
|
@ -675,16 +778,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
|
|
||||||
it('passes notes through', () => {
|
it('passes notes through', () => {
|
||||||
const sequence = generator.generate({stages: [
|
const sequence = generator.generate({stages: [
|
||||||
PARSED.note(['A', 'B'], {
|
PARSED.note('note right', ['A', 'B'], {
|
||||||
type: 'note right',
|
|
||||||
mode: 'foo',
|
mode: 'foo',
|
||||||
label: 'bar',
|
label: 'bar',
|
||||||
}),
|
}),
|
||||||
]});
|
]});
|
||||||
expect(sequence.stages).toEqual([
|
expect(sequence.stages).toEqual([
|
||||||
jasmine.anything(),
|
jasmine.anything(),
|
||||||
GENERATED.note(['A', 'B'], {
|
GENERATED.note('note right', ['A', 'B'], {
|
||||||
type: 'note right',
|
|
||||||
mode: 'foo',
|
mode: 'foo',
|
||||||
label: 'bar',
|
label: 'bar',
|
||||||
}),
|
}),
|
||||||
|
@ -694,14 +795,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
|
||||||
|
|
||||||
it('defaults to showing notes around the entire diagram', () => {
|
it('defaults to showing notes around the entire diagram', () => {
|
||||||
const sequence = generator.generate({stages: [
|
const sequence = generator.generate({stages: [
|
||||||
PARSED.note([], {type: 'note right'}),
|
PARSED.note('note right', []),
|
||||||
PARSED.note([], {type: 'note left'}),
|
PARSED.note('note left', []),
|
||||||
PARSED.note([], {type: 'note over'}),
|
PARSED.note('note over', []),
|
||||||
]});
|
]});
|
||||||
expect(sequence.stages).toEqual([
|
expect(sequence.stages).toEqual([
|
||||||
GENERATED.note([']'], {type: 'note right'}),
|
GENERATED.note('note right', [']']),
|
||||||
GENERATED.note(['['], {type: 'note left'}),
|
GENERATED.note('note left', ['[']),
|
||||||
GENERATED.note(['[', ']'], {type: 'note over'}),
|
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([
|
define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
|
||||||
'core/ArrayUtilities',
|
|
||||||
'svg/SVGUtilities',
|
|
||||||
'svg/SVGTextBlock',
|
|
||||||
'svg/SVGShapes',
|
|
||||||
], (
|
|
||||||
array,
|
|
||||||
svg,
|
|
||||||
SVGTextBlock,
|
|
||||||
SVGShapes
|
|
||||||
) => {
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const LINE_HEIGHT = 1.3;
|
const LINE_HEIGHT = 1.3;
|
||||||
|
@ -18,6 +8,7 @@ define([
|
||||||
outerMargin: 5,
|
outerMargin: 5,
|
||||||
agentMargin: 10,
|
agentMargin: 10,
|
||||||
actionMargin: 5,
|
actionMargin: 5,
|
||||||
|
agentLineHighlightRadius: 4,
|
||||||
|
|
||||||
agentCap: {
|
agentCap: {
|
||||||
box: {
|
box: {
|
||||||
|
|
|
@ -9,5 +9,10 @@ define([
|
||||||
'sequence/Generator_spec',
|
'sequence/Generator_spec',
|
||||||
'sequence/Renderer_spec',
|
'sequence/Renderer_spec',
|
||||||
'sequence/themes/Basic_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',
|
'sequence/sequence_integration_spec',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -106,5 +106,6 @@ define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => {
|
||||||
renderBox,
|
renderBox,
|
||||||
renderNote,
|
renderNote,
|
||||||
renderBoxedText,
|
renderBoxedText,
|
||||||
|
TextBlock: SVGTextBlock,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue