Refactor to enable formatted text everywhere, and make 'agent' dichotomy in generator clearer through naming [#29]
This commit is contained in:
parent
2b5f7eea2b
commit
0f22dc7f94
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -21,35 +21,52 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
AgentState.LOCKED = new AgentState({locked: true});
|
||||
AgentState.DEFAULT = new AgentState();
|
||||
|
||||
const Agent = {
|
||||
// Agent from Parser: {name, flags}
|
||||
const PAgent = {
|
||||
equals: (a, b) => {
|
||||
return a.name === b.name;
|
||||
},
|
||||
make: (name, {anchorRight = false} = {}) => {
|
||||
return {name, anchorRight};
|
||||
},
|
||||
getName: (agent) => {
|
||||
return agent.name;
|
||||
},
|
||||
hasFlag: (flag, has = true) => {
|
||||
return (agent) => (agent.flags.includes(flag) === has);
|
||||
return (pAgent) => (pAgent.flags.includes(flag) === has);
|
||||
},
|
||||
};
|
||||
|
||||
// Agent from Generator: {id, formattedLabel, anchorRight}
|
||||
const GAgent = {
|
||||
equals: (a, b) => {
|
||||
return a.id === b.id;
|
||||
},
|
||||
make: (id, {anchorRight = false} = {}) => {
|
||||
return {id, anchorRight};
|
||||
},
|
||||
indexOf: (list, gAgent) => {
|
||||
return array.indexOf(list, gAgent, GAgent.equals);
|
||||
},
|
||||
hasIntersection: (a, b) => {
|
||||
return array.hasIntersection(a, b, GAgent.equals);
|
||||
},
|
||||
};
|
||||
|
||||
const NOTE_DEFAULT_G_AGENTS = {
|
||||
'note over': [GAgent.make('['), GAgent.make(']')],
|
||||
'note left': [GAgent.make('[')],
|
||||
'note right': [GAgent.make(']')],
|
||||
};
|
||||
|
||||
const MERGABLE = {
|
||||
'agent begin': {
|
||||
check: ['mode'],
|
||||
merge: ['agentNames'],
|
||||
merge: ['agentIDs'],
|
||||
siblings: new Set(['agent highlight']),
|
||||
},
|
||||
'agent end': {
|
||||
check: ['mode'],
|
||||
merge: ['agentNames'],
|
||||
merge: ['agentIDs'],
|
||||
siblings: new Set(['agent highlight']),
|
||||
},
|
||||
'agent highlight': {
|
||||
check: ['highlighted'],
|
||||
merge: ['agentNames'],
|
||||
merge: ['agentIDs'],
|
||||
siblings: new Set(['agent begin', 'agent end']),
|
||||
},
|
||||
};
|
||||
|
@ -183,39 +200,33 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
}
|
||||
|
||||
function addBounds(target, agentL, agentR, involvedAgents = null) {
|
||||
array.remove(target, agentL, Agent.equals);
|
||||
array.remove(target, agentR, Agent.equals);
|
||||
function addBounds(allGAgents, gAgentL, gAgentR, involvedGAgents = null) {
|
||||
array.remove(allGAgents, gAgentL, GAgent.equals);
|
||||
array.remove(allGAgents, gAgentR, GAgent.equals);
|
||||
|
||||
let indexL = 0;
|
||||
let indexR = target.length;
|
||||
if(involvedAgents) {
|
||||
const found = (involvedAgents
|
||||
.map((agent) => array.indexOf(target, agent, Agent.equals))
|
||||
let indexR = allGAgents.length;
|
||||
if(involvedGAgents) {
|
||||
const found = (involvedGAgents
|
||||
.map((gAgent) => GAgent.indexOf(allGAgents, gAgent))
|
||||
.filter((p) => (p !== -1))
|
||||
);
|
||||
indexL = found.reduce((a, b) => Math.min(a, b), target.length);
|
||||
indexL = found.reduce((a, b) => Math.min(a, b), allGAgents.length);
|
||||
indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1;
|
||||
}
|
||||
|
||||
target.splice(indexL, 0, agentL);
|
||||
target.splice(indexR + 1, 0, agentR);
|
||||
allGAgents.splice(indexL, 0, gAgentL);
|
||||
allGAgents.splice(indexR + 1, 0, gAgentR);
|
||||
|
||||
return {indexL, indexR: indexR + 1};
|
||||
}
|
||||
|
||||
const NOTE_DEFAULT_AGENTS = {
|
||||
'note over': [{name: '[', flags: []}, {name: ']', flags: []}],
|
||||
'note left': [{name: '[', flags: []}],
|
||||
'note right': [{name: ']', flags: []}],
|
||||
};
|
||||
|
||||
return class Generator {
|
||||
constructor() {
|
||||
this.agentStates = new Map();
|
||||
this.agentAliases = new Map();
|
||||
this.activeGroups = new Map();
|
||||
this.agents = [];
|
||||
this.gAgents = [];
|
||||
this.labelPattern = null;
|
||||
this.blockCount = 0;
|
||||
this.nesting = [];
|
||||
|
@ -240,13 +251,13 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
'note right': this.handleNote.bind(this),
|
||||
'note between': this.handleNote.bind(this),
|
||||
};
|
||||
this.expandGroupedAgent = this.expandGroupedAgent.bind(this);
|
||||
this.expandGroupedGAgent = this.expandGroupedGAgent.bind(this);
|
||||
this.handleStage = this.handleStage.bind(this);
|
||||
this.convertAgent = this.convertAgent.bind(this);
|
||||
this.toGAgent = this.toGAgent.bind(this);
|
||||
this.endGroup = this.endGroup.bind(this);
|
||||
}
|
||||
|
||||
convertAgent({alias, name}) {
|
||||
toGAgent({alias, name}) {
|
||||
if(alias) {
|
||||
if(this.agentAliases.has(name)) {
|
||||
throw new Error(
|
||||
|
@ -256,7 +267,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
const old = this.agentAliases.get(alias);
|
||||
if(
|
||||
(old && old !== alias) ||
|
||||
this.agents.some((agent) => (agent.name === alias))
|
||||
this.gAgents.some((gAgent) => (gAgent.id === alias))
|
||||
) {
|
||||
throw new Error(
|
||||
'Cannot use ' + alias +
|
||||
|
@ -265,7 +276,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
this.agentAliases.set(alias, name);
|
||||
}
|
||||
return Agent.make(this.agentAliases.get(name) || name);
|
||||
return GAgent.make(this.agentAliases.get(name) || name);
|
||||
}
|
||||
|
||||
addStage(stage, isVisible = true) {
|
||||
|
@ -300,138 +311,139 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
});
|
||||
}
|
||||
|
||||
defineAgents(colAgents) {
|
||||
array.mergeSets(this.currentNest.agents, colAgents, Agent.equals);
|
||||
array.mergeSets(this.agents, colAgents, Agent.equals);
|
||||
defineGAgents(gAgents) {
|
||||
array.mergeSets(this.currentNest.gAgents, gAgents, GAgent.equals);
|
||||
array.mergeSets(this.gAgents, gAgents, GAgent.equals);
|
||||
}
|
||||
|
||||
getAgentState(agent) {
|
||||
return this.agentStates.get(agent.name) || AgentState.DEFAULT;
|
||||
getGAgentState(gAgent) {
|
||||
return this.agentStates.get(gAgent.id) || AgentState.DEFAULT;
|
||||
}
|
||||
|
||||
updateAgentState(agent, change) {
|
||||
const state = this.agentStates.get(agent.name);
|
||||
updateGAgentState(gAgent, change) {
|
||||
const state = this.agentStates.get(gAgent.id);
|
||||
if(state) {
|
||||
Object.assign(state, change);
|
||||
} else {
|
||||
this.agentStates.set(agent.name, new AgentState(change));
|
||||
this.agentStates.set(gAgent.id, new AgentState(change));
|
||||
}
|
||||
}
|
||||
|
||||
validateAgents(agents, {
|
||||
replaceGAgentState(gAgent, state) {
|
||||
this.agentStates.set(gAgent.id, state);
|
||||
}
|
||||
|
||||
validateGAgents(gAgents, {
|
||||
allowGrouped = false,
|
||||
rejectGrouped = false,
|
||||
} = {}) {
|
||||
agents.forEach((agent) => {
|
||||
const state = this.getAgentState(agent);
|
||||
gAgents.forEach((gAgent) => {
|
||||
const state = this.getGAgentState(gAgent);
|
||||
if(state.covered) {
|
||||
throw new Error(
|
||||
'Agent ' + agent.name + ' is hidden behind group'
|
||||
'Agent ' + gAgent.id + ' is hidden behind group'
|
||||
);
|
||||
}
|
||||
if(rejectGrouped && state.group !== null) {
|
||||
throw new Error('Agent ' + agent.name + ' is in a group');
|
||||
throw new Error('Agent ' + gAgent.id + ' is in a group');
|
||||
}
|
||||
if(state.blocked && (!allowGrouped || state.group === null)) {
|
||||
throw new Error('Duplicate agent name: ' + agent.name);
|
||||
throw new Error('Duplicate agent name: ' + gAgent.id);
|
||||
}
|
||||
if(agent.name.startsWith('__')) {
|
||||
throw new Error(agent.name + ' is a reserved name');
|
||||
if(gAgent.id.startsWith('__')) {
|
||||
throw new Error(gAgent.id + ' is a reserved name');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAgentVis(colAgents, visible, mode, checked = false) {
|
||||
setGAgentVis(gAgents, visible, mode, checked = false) {
|
||||
const seen = new Set();
|
||||
const filteredAgents = colAgents.filter((agent) => {
|
||||
if(seen.has(agent.name)) {
|
||||
const filteredGAgents = gAgents.filter((gAgent) => {
|
||||
if(seen.has(gAgent.id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(agent.name);
|
||||
const state = this.getAgentState(agent);
|
||||
seen.add(gAgent.id);
|
||||
const state = this.getGAgentState(gAgent);
|
||||
if(state.locked || state.blocked) {
|
||||
if(checked) {
|
||||
throw new Error(
|
||||
'Cannot begin/end agent: ' + agent.name
|
||||
);
|
||||
throw new Error('Cannot begin/end agent: ' + gAgent.id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return state.visible !== visible;
|
||||
});
|
||||
if(filteredAgents.length === 0) {
|
||||
if(filteredGAgents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
filteredAgents.forEach((agent) => {
|
||||
this.updateAgentState(agent, {visible});
|
||||
filteredGAgents.forEach((gAgent) => {
|
||||
this.updateGAgentState(gAgent, {visible});
|
||||
});
|
||||
this.defineAgents(filteredAgents);
|
||||
this.defineGAgents(filteredGAgents);
|
||||
|
||||
return {
|
||||
type: (visible ? 'agent begin' : 'agent end'),
|
||||
agentNames: filteredAgents.map(Agent.getName),
|
||||
agentIDs: filteredGAgents.map((gAgent) => gAgent.id),
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
setAgentHighlight(colAgents, highlighted, checked = false) {
|
||||
const filteredAgents = colAgents.filter((agent) => {
|
||||
const state = this.getAgentState(agent);
|
||||
setGAgentHighlight(gAgents, highlighted, checked = false) {
|
||||
const filteredGAgents = gAgents.filter((gAgent) => {
|
||||
const state = this.getGAgentState(gAgent);
|
||||
if(state.locked || state.blocked) {
|
||||
if(checked) {
|
||||
throw new Error(
|
||||
'Cannot highlight agent: ' + agent.name
|
||||
);
|
||||
throw new Error('Cannot highlight agent: ' + gAgent.id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return state.visible && (state.highlighted !== highlighted);
|
||||
});
|
||||
if(filteredAgents.length === 0) {
|
||||
if(filteredGAgents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
filteredAgents.forEach((agent) => {
|
||||
this.updateAgentState(agent, {highlighted});
|
||||
filteredGAgents.forEach((gAgent) => {
|
||||
this.updateGAgentState(gAgent, {highlighted});
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'agent highlight',
|
||||
agentNames: filteredAgents.map(Agent.getName),
|
||||
agentIDs: filteredGAgents.map((gAgent) => gAgent.id),
|
||||
highlighted,
|
||||
};
|
||||
}
|
||||
|
||||
beginNested(mode, label, name, ln) {
|
||||
const leftAgent = Agent.make(name + '[', {anchorRight: true});
|
||||
const rightAgent = Agent.make(name + ']');
|
||||
const agents = [leftAgent, rightAgent];
|
||||
beginNested(blockType, {tag, label, name, ln}) {
|
||||
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
|
||||
const rightGAgent = GAgent.make(name + ']');
|
||||
const gAgents = [leftGAgent, rightGAgent];
|
||||
const stages = [];
|
||||
this.currentSection = {
|
||||
header: {
|
||||
type: 'block begin',
|
||||
mode,
|
||||
label,
|
||||
left: leftAgent.name,
|
||||
right: rightAgent.name,
|
||||
blockType,
|
||||
tag: this.textFormatter(tag),
|
||||
label: this.textFormatter(label),
|
||||
left: leftGAgent.id,
|
||||
right: rightGAgent.id,
|
||||
ln,
|
||||
},
|
||||
stages,
|
||||
};
|
||||
this.currentNest = {
|
||||
mode,
|
||||
agents,
|
||||
leftAgent,
|
||||
rightAgent,
|
||||
blockType,
|
||||
gAgents,
|
||||
leftGAgent,
|
||||
rightGAgent,
|
||||
hasContent: false,
|
||||
sections: [this.currentSection],
|
||||
};
|
||||
this.agentStates.set(leftAgent.name, AgentState.LOCKED);
|
||||
this.agentStates.set(rightAgent.name, AgentState.LOCKED);
|
||||
this.replaceGAgentState(leftGAgent, AgentState.LOCKED);
|
||||
this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
|
||||
this.nesting.push(this.currentNest);
|
||||
|
||||
return {agents, stages};
|
||||
return {gAgents, stages};
|
||||
}
|
||||
|
||||
nextBlockName() {
|
||||
|
@ -440,25 +452,31 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
return name;
|
||||
}
|
||||
|
||||
handleBlockBegin({ln, mode, label}) {
|
||||
this.beginNested(mode, label, this.nextBlockName(), ln);
|
||||
handleBlockBegin({ln, blockType, tag, label}) {
|
||||
this.beginNested(blockType, {
|
||||
tag,
|
||||
label,
|
||||
name: this.nextBlockName(),
|
||||
ln,
|
||||
});
|
||||
}
|
||||
|
||||
handleBlockSplit({ln, mode, label}) {
|
||||
if(this.currentNest.mode !== 'if') {
|
||||
handleBlockSplit({ln, blockType, tag, label}) {
|
||||
if(this.currentNest.blockType !== 'if') {
|
||||
throw new Error(
|
||||
'Invalid block nesting ("else" inside ' +
|
||||
this.currentNest.mode + ')'
|
||||
this.currentNest.blockType + ')'
|
||||
);
|
||||
}
|
||||
optimiseStages(this.currentSection.stages);
|
||||
this.currentSection = {
|
||||
header: {
|
||||
type: 'block split',
|
||||
mode,
|
||||
label,
|
||||
left: this.currentNest.leftAgent.name,
|
||||
right: this.currentNest.rightAgent.name,
|
||||
blockType,
|
||||
tag: this.textFormatter(tag),
|
||||
label: this.textFormatter(label),
|
||||
left: this.currentNest.leftGAgent.id,
|
||||
right: this.currentNest.rightGAgent.id,
|
||||
ln,
|
||||
},
|
||||
stages: [],
|
||||
|
@ -476,12 +494,12 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
this.currentSection = array.last(this.currentNest.sections);
|
||||
|
||||
if(nested.hasContent) {
|
||||
this.defineAgents(nested.agents);
|
||||
this.defineGAgents(nested.gAgents);
|
||||
addBounds(
|
||||
this.agents,
|
||||
nested.leftAgent,
|
||||
nested.rightAgent,
|
||||
nested.agents
|
||||
this.gAgents,
|
||||
nested.leftGAgent,
|
||||
nested.rightGAgent,
|
||||
nested.gAgents
|
||||
);
|
||||
nested.sections.forEach((section) => {
|
||||
this.currentSection.stages.push(section.header);
|
||||
|
@ -489,70 +507,71 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
});
|
||||
this.addStage({
|
||||
type: 'block end',
|
||||
left: nested.leftAgent.name,
|
||||
right: nested.rightAgent.name,
|
||||
left: nested.leftGAgent.id,
|
||||
right: nested.rightGAgent.id,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Empty block');
|
||||
}
|
||||
}
|
||||
|
||||
makeGroupDetails(agents, alias) {
|
||||
const colAgents = agents.map(this.convertAgent);
|
||||
this.validateAgents(colAgents, {rejectGrouped: true});
|
||||
makeGroupDetails(pAgents, alias) {
|
||||
const gAgents = pAgents.map(this.toGAgent);
|
||||
this.validateGAgents(gAgents, {rejectGrouped: true});
|
||||
if(this.agentStates.has(alias)) {
|
||||
throw new Error('Duplicate agent name: ' + alias);
|
||||
}
|
||||
const name = this.nextBlockName();
|
||||
const leftAgent = Agent.make(name + '[', {anchorRight: true});
|
||||
const rightAgent = Agent.make(name + ']');
|
||||
this.agentStates.set(leftAgent.name, AgentState.LOCKED);
|
||||
this.agentStates.set(rightAgent.name, AgentState.LOCKED);
|
||||
this.updateAgentState(
|
||||
{name: alias},
|
||||
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
|
||||
const rightGAgent = GAgent.make(name + ']');
|
||||
this.replaceGAgentState(leftGAgent, AgentState.LOCKED);
|
||||
this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
|
||||
this.updateGAgentState(
|
||||
GAgent.make(alias),
|
||||
{blocked: true, group: alias}
|
||||
);
|
||||
this.defineAgents(colAgents);
|
||||
this.defineGAgents(gAgents);
|
||||
const {indexL, indexR} = addBounds(
|
||||
this.agents,
|
||||
leftAgent,
|
||||
rightAgent,
|
||||
colAgents
|
||||
this.gAgents,
|
||||
leftGAgent,
|
||||
rightGAgent,
|
||||
gAgents
|
||||
);
|
||||
|
||||
const agentsCovered = [];
|
||||
const agentsContained = colAgents.slice();
|
||||
const gAgentsCovered = [];
|
||||
const gAgentsContained = gAgents.slice();
|
||||
for(let i = indexL + 1; i < indexR; ++ i) {
|
||||
agentsCovered.push(this.agents[i]);
|
||||
gAgentsCovered.push(this.gAgents[i]);
|
||||
}
|
||||
array.removeAll(agentsCovered, agentsContained, Agent.equals);
|
||||
array.removeAll(gAgentsCovered, gAgentsContained, GAgent.equals);
|
||||
|
||||
return {
|
||||
colAgents,
|
||||
leftAgent,
|
||||
rightAgent,
|
||||
agentsContained,
|
||||
agentsCovered,
|
||||
gAgents,
|
||||
leftGAgent,
|
||||
rightGAgent,
|
||||
gAgentsContained,
|
||||
gAgentsCovered,
|
||||
};
|
||||
}
|
||||
|
||||
handleGroupBegin({agents, mode, label, alias}) {
|
||||
handleGroupBegin({agents, blockType, tag, label, alias}) {
|
||||
const details = this.makeGroupDetails(agents, alias);
|
||||
|
||||
details.agentsContained.forEach((agent) => {
|
||||
this.updateAgentState(agent, {group: alias});
|
||||
details.gAgentsContained.forEach((gAgent) => {
|
||||
this.updateGAgentState(gAgent, {group: alias});
|
||||
});
|
||||
details.agentsCovered.forEach((agent) => {
|
||||
this.updateAgentState(agent, {covered: true});
|
||||
details.gAgentsCovered.forEach((gAgent) => {
|
||||
this.updateGAgentState(gAgent, {covered: true});
|
||||
});
|
||||
this.activeGroups.set(alias, details);
|
||||
this.addStage(this.setAgentVis(details.colAgents, true, 'box'));
|
||||
this.addStage(this.setGAgentVis(details.gAgents, true, 'box'));
|
||||
this.addStage({
|
||||
type: 'block begin',
|
||||
mode,
|
||||
label,
|
||||
left: details.leftAgent.name,
|
||||
right: details.rightAgent.name,
|
||||
blockType,
|
||||
tag: this.textFormatter(tag),
|
||||
label: this.textFormatter(label),
|
||||
left: details.leftGAgent.id,
|
||||
right: details.rightGAgent.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -563,18 +582,18 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
this.activeGroups.delete(name);
|
||||
|
||||
details.agentsContained.forEach((agent) => {
|
||||
this.updateAgentState(agent, {group: null});
|
||||
details.gAgentsContained.forEach((gAgent) => {
|
||||
this.updateGAgentState(gAgent, {group: null});
|
||||
});
|
||||
details.agentsCovered.forEach((agent) => {
|
||||
this.updateAgentState(agent, {covered: false});
|
||||
details.gAgentsCovered.forEach((gAgent) => {
|
||||
this.updateGAgentState(gAgent, {covered: false});
|
||||
});
|
||||
this.updateAgentState({name}, {group: null});
|
||||
this.updateGAgentState(GAgent.make(name), {group: null});
|
||||
|
||||
return {
|
||||
type: 'block end',
|
||||
left: details.leftAgent.name,
|
||||
right: details.rightAgent.name,
|
||||
left: details.leftGAgent.id,
|
||||
right: details.rightGAgent.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -620,156 +639,156 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
return result;
|
||||
}
|
||||
|
||||
expandGroupedAgent(agent) {
|
||||
const group = this.getAgentState(agent).group;
|
||||
expandGroupedGAgent(gAgent) {
|
||||
const group = this.getGAgentState(gAgent).group;
|
||||
if(!group) {
|
||||
return [agent];
|
||||
return [gAgent];
|
||||
}
|
||||
const details = this.activeGroups.get(group);
|
||||
return [details.leftAgent, details.rightAgent];
|
||||
return [details.leftGAgent, details.rightGAgent];
|
||||
}
|
||||
|
||||
expandGroupedAgentConnection(agents) {
|
||||
const agents1 = this.expandGroupedAgent(agents[0]);
|
||||
const agents2 = this.expandGroupedAgent(agents[1]);
|
||||
let ind1 = array.indexOf(this.agents, agents1[0], Agent.equals);
|
||||
let ind2 = array.indexOf(this.agents, agents2[0], Agent.equals);
|
||||
expandGroupedGAgentConnection(gAgents) {
|
||||
const gAgents1 = this.expandGroupedGAgent(gAgents[0]);
|
||||
const gAgents2 = this.expandGroupedGAgent(gAgents[1]);
|
||||
let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]);
|
||||
let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]);
|
||||
if(ind1 === -1) {
|
||||
ind1 = this.agents.length;
|
||||
ind1 = this.gAgents.length;
|
||||
}
|
||||
if(ind2 === -1) {
|
||||
ind2 = this.agents.length;
|
||||
ind2 = this.gAgents.length;
|
||||
}
|
||||
if(ind1 === ind2) {
|
||||
// Self-connection
|
||||
return [array.last(agents1), array.last(agents2)];
|
||||
return [array.last(gAgents1), array.last(gAgents2)];
|
||||
} else if(ind1 < ind2) {
|
||||
return [array.last(agents1), agents2[0]];
|
||||
return [array.last(gAgents1), gAgents2[0]];
|
||||
} else {
|
||||
return [agents1[0], array.last(agents2)];
|
||||
return [gAgents1[0], array.last(gAgents2)];
|
||||
}
|
||||
}
|
||||
|
||||
filterConnectFlags(agents) {
|
||||
const beginAgents = (agents
|
||||
.filter(Agent.hasFlag('begin'))
|
||||
.map(this.convertAgent)
|
||||
filterConnectFlags(pAgents) {
|
||||
const beginGAgents = (pAgents
|
||||
.filter(PAgent.hasFlag('begin'))
|
||||
.map(this.toGAgent)
|
||||
);
|
||||
const endAgents = (agents
|
||||
.filter(Agent.hasFlag('end'))
|
||||
.map(this.convertAgent)
|
||||
const endGAgents = (pAgents
|
||||
.filter(PAgent.hasFlag('end'))
|
||||
.map(this.toGAgent)
|
||||
);
|
||||
if(array.hasIntersection(beginAgents, endAgents, Agent.equals)) {
|
||||
if(GAgent.hasIntersection(beginGAgents, endGAgents)) {
|
||||
throw new Error('Cannot set agent visibility multiple times');
|
||||
}
|
||||
|
||||
const startAgents = (agents
|
||||
.filter(Agent.hasFlag('start'))
|
||||
.map(this.convertAgent)
|
||||
const startGAgents = (pAgents
|
||||
.filter(PAgent.hasFlag('start'))
|
||||
.map(this.toGAgent)
|
||||
);
|
||||
const stopAgents = (agents
|
||||
.filter(Agent.hasFlag('stop'))
|
||||
.map(this.convertAgent)
|
||||
const stopGAgents = (pAgents
|
||||
.filter(PAgent.hasFlag('stop'))
|
||||
.map(this.toGAgent)
|
||||
);
|
||||
array.mergeSets(stopAgents, endAgents);
|
||||
if(array.hasIntersection(startAgents, stopAgents, Agent.equals)) {
|
||||
array.mergeSets(stopGAgents, endGAgents);
|
||||
if(GAgent.hasIntersection(startGAgents, stopGAgents)) {
|
||||
throw new Error('Cannot set agent highlighting multiple times');
|
||||
}
|
||||
|
||||
this.validateAgents(beginAgents);
|
||||
this.validateAgents(endAgents);
|
||||
this.validateAgents(startAgents);
|
||||
this.validateAgents(stopAgents);
|
||||
this.validateGAgents(beginGAgents);
|
||||
this.validateGAgents(endGAgents);
|
||||
this.validateGAgents(startGAgents);
|
||||
this.validateGAgents(stopGAgents);
|
||||
|
||||
return {beginAgents, endAgents, startAgents, stopAgents};
|
||||
return {beginGAgents, endGAgents, startGAgents, stopGAgents};
|
||||
}
|
||||
|
||||
handleConnect({agents, label, options}) {
|
||||
const flags = this.filterConnectFlags(agents);
|
||||
|
||||
let colAgents = agents.map(this.convertAgent);
|
||||
this.validateAgents(colAgents, {allowGrouped: true});
|
||||
let gAgents = agents.map(this.toGAgent);
|
||||
this.validateGAgents(gAgents, {allowGrouped: true});
|
||||
|
||||
const allAgents = array.flatMap(colAgents, this.expandGroupedAgent);
|
||||
this.defineAgents(allAgents);
|
||||
const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
|
||||
this.defineGAgents(allGAgents);
|
||||
|
||||
colAgents = this.expandGroupedAgentConnection(colAgents);
|
||||
const agentNames = colAgents.map(Agent.getName);
|
||||
gAgents = this.expandGroupedGAgentConnection(gAgents);
|
||||
const agentIDs = gAgents.map((gAgent) => gAgent.id);
|
||||
|
||||
const implicitBegin = (agents
|
||||
.filter(Agent.hasFlag('begin', false))
|
||||
.map(this.convertAgent)
|
||||
const implicitBeginGAgents = (agents
|
||||
.filter(PAgent.hasFlag('begin', false))
|
||||
.map(this.toGAgent)
|
||||
);
|
||||
this.addStage(this.setAgentVis(implicitBegin, true, 'box'));
|
||||
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
|
||||
|
||||
const connectStage = {
|
||||
type: 'connect',
|
||||
agentNames,
|
||||
label: this.applyLabelPattern(label),
|
||||
agentIDs,
|
||||
label: this.textFormatter(this.applyLabelPattern(label)),
|
||||
options,
|
||||
};
|
||||
|
||||
this.addParallelStages([
|
||||
this.setAgentVis(flags.beginAgents, true, 'box', true),
|
||||
this.setAgentHighlight(flags.startAgents, true, true),
|
||||
this.setGAgentVis(flags.beginGAgents, true, 'box', true),
|
||||
this.setGAgentHighlight(flags.startGAgents, true, true),
|
||||
connectStage,
|
||||
this.setAgentHighlight(flags.stopAgents, false, true),
|
||||
this.setAgentVis(flags.endAgents, false, 'cross', true),
|
||||
this.setGAgentHighlight(flags.stopGAgents, false, true),
|
||||
this.setGAgentVis(flags.endGAgents, false, 'cross', true),
|
||||
]);
|
||||
}
|
||||
|
||||
handleNote({type, agents, mode, label}) {
|
||||
let colAgents = null;
|
||||
let gAgents = null;
|
||||
if(agents.length === 0) {
|
||||
colAgents = NOTE_DEFAULT_AGENTS[type] || [];
|
||||
gAgents = NOTE_DEFAULT_G_AGENTS[type] || [];
|
||||
} else {
|
||||
colAgents = agents.map(this.convertAgent);
|
||||
gAgents = agents.map(this.toGAgent);
|
||||
}
|
||||
|
||||
this.validateAgents(colAgents, {allowGrouped: true});
|
||||
colAgents = array.flatMap(colAgents, this.expandGroupedAgent);
|
||||
const agentNames = colAgents.map(Agent.getName);
|
||||
const uniqueAgents = new Set(agentNames).size;
|
||||
this.validateGAgents(gAgents, {allowGrouped: true});
|
||||
gAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
|
||||
const agentIDs = gAgents.map((gAgent) => gAgent.id);
|
||||
const uniqueAgents = new Set(agentIDs).size;
|
||||
if(type === 'note between' && uniqueAgents < 2) {
|
||||
throw new Error('note between requires at least 2 agents');
|
||||
}
|
||||
|
||||
this.addStage(this.setAgentVis(colAgents, true, 'box'));
|
||||
this.defineAgents(colAgents);
|
||||
this.addStage(this.setGAgentVis(gAgents, true, 'box'));
|
||||
this.defineGAgents(gAgents);
|
||||
|
||||
this.addStage({
|
||||
type,
|
||||
agentNames,
|
||||
agentIDs,
|
||||
mode,
|
||||
label,
|
||||
label: this.textFormatter(label),
|
||||
});
|
||||
}
|
||||
|
||||
handleAgentDefine({agents}) {
|
||||
const colAgents = agents.map(this.convertAgent);
|
||||
this.validateAgents(colAgents);
|
||||
this.defineAgents(colAgents);
|
||||
const gAgents = agents.map(this.toGAgent);
|
||||
this.validateGAgents(gAgents);
|
||||
this.defineGAgents(gAgents);
|
||||
}
|
||||
|
||||
handleAgentBegin({agents, mode}) {
|
||||
const colAgents = agents.map(this.convertAgent);
|
||||
this.validateAgents(colAgents);
|
||||
this.addStage(this.setAgentVis(colAgents, true, mode, true));
|
||||
const gAgents = agents.map(this.toGAgent);
|
||||
this.validateGAgents(gAgents);
|
||||
this.addStage(this.setGAgentVis(gAgents, true, mode, true));
|
||||
}
|
||||
|
||||
handleAgentEnd({agents, mode}) {
|
||||
const groupAgents = (agents
|
||||
.filter((agent) => this.activeGroups.has(agent.name))
|
||||
const groupPAgents = (agents
|
||||
.filter((pAgent) => this.activeGroups.has(pAgent.name))
|
||||
);
|
||||
const colAgents = (agents
|
||||
.filter((agent) => !this.activeGroups.has(agent.name))
|
||||
.map(this.convertAgent)
|
||||
const gAgents = (agents
|
||||
.filter((pAgent) => !this.activeGroups.has(pAgent.name))
|
||||
.map(this.toGAgent)
|
||||
);
|
||||
this.validateAgents(colAgents);
|
||||
this.validateGAgents(gAgents);
|
||||
this.addParallelStages([
|
||||
this.setAgentHighlight(colAgents, false),
|
||||
this.setAgentVis(colAgents, false, mode, true),
|
||||
...groupAgents.map(this.endGroup),
|
||||
this.setGAgentHighlight(gAgents, false),
|
||||
this.setGAgentVis(gAgents, false, mode, true),
|
||||
...groupPAgents.map(this.endGroup),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -783,21 +802,46 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
handler(stage);
|
||||
} catch(e) {
|
||||
if(typeof e === 'object' && e.message) {
|
||||
throw new Error(e.message + ' at line ' + (stage.ln + 1));
|
||||
e.message += ' at line ' + (stage.ln + 1);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate({stages, meta = {}}) {
|
||||
_reset() {
|
||||
this.agentStates.clear();
|
||||
this.markers.clear();
|
||||
this.agentAliases.clear();
|
||||
this.activeGroups.clear();
|
||||
this.agents.length = 0;
|
||||
this.gAgents.length = 0;
|
||||
this.blockCount = 0;
|
||||
this.nesting.length = 0;
|
||||
this.labelPattern = [{token: 'label'}];
|
||||
const globals = this.beginNested('global', '', '', 0);
|
||||
}
|
||||
|
||||
_finalise(globals) {
|
||||
addBounds(
|
||||
this.gAgents,
|
||||
this.currentNest.leftGAgent,
|
||||
this.currentNest.rightGAgent
|
||||
);
|
||||
optimiseStages(globals.stages);
|
||||
|
||||
this.gAgents.forEach((gAgent) => {
|
||||
gAgent.formattedLabel = this.textFormatter(gAgent.id);
|
||||
});
|
||||
}
|
||||
|
||||
generate({stages, meta = {}}) {
|
||||
this._reset();
|
||||
|
||||
this.textFormatter = meta.textFormatter;
|
||||
const globals = this.beginNested('global', {
|
||||
tag: '',
|
||||
label: '',
|
||||
name: '',
|
||||
ln: 0,
|
||||
});
|
||||
|
||||
stages.forEach(this.handleStage);
|
||||
|
||||
|
@ -812,26 +856,21 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
|
||||
const terminators = meta.terminators || 'none';
|
||||
|
||||
this.addParallelStages([
|
||||
this.setAgentHighlight(this.agents, false),
|
||||
this.setAgentVis(this.agents, false, terminators),
|
||||
this.setGAgentHighlight(this.gAgents, false),
|
||||
this.setGAgentVis(this.gAgents, false, terminators),
|
||||
]);
|
||||
|
||||
addBounds(
|
||||
this.agents,
|
||||
this.currentNest.leftAgent,
|
||||
this.currentNest.rightAgent
|
||||
);
|
||||
optimiseStages(globals.stages);
|
||||
this._finalise(globals);
|
||||
|
||||
swapFirstBegin(globals.stages, meta.headers || 'box');
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: meta.title,
|
||||
title: this.textFormatter(meta.title),
|
||||
theme: meta.theme,
|
||||
},
|
||||
agents: this.agents.slice(),
|
||||
agents: this.gAgents.slice(),
|
||||
stages: globals.stages,
|
||||
};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
|||
define(() => {
|
||||
'use strict';
|
||||
|
||||
function parseMarkdown(text) {
|
||||
if(!text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = text.split('\n');
|
||||
const result = [];
|
||||
const attrs = null;
|
||||
lines.forEach((line) => {
|
||||
result.push([{text: line, attrs}]);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
return parseMarkdown;
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
defineDescribe('Markdown Parser', [
|
||||
'./MarkdownParser',
|
||||
'svg/SVGTextBlock',
|
||||
'svg/SVGUtilities',
|
||||
], (
|
||||
parser,
|
||||
SVGTextBlock,
|
||||
svg
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
it('converts simple text', () => {
|
||||
const formatted = parser('hello everybody');
|
||||
expect(formatted).toEqual([[{text: 'hello everybody', attrs: null}]]);
|
||||
});
|
||||
|
||||
it('produces an empty array given an empty input', () => {
|
||||
const formatted = parser('');
|
||||
expect(formatted).toEqual([]);
|
||||
});
|
||||
|
||||
it('converts multiline text', () => {
|
||||
const formatted = parser('hello\neverybody');
|
||||
expect(formatted).toEqual([
|
||||
[{text: 'hello', attrs: null}],
|
||||
[{text: 'everybody', attrs: null}],
|
||||
]);
|
||||
});
|
||||
|
||||
describe('SVGTextBlock interaction', () => {
|
||||
let hold = null;
|
||||
let block = null;
|
||||
|
||||
beforeEach(() => {
|
||||
hold = svg.makeContainer();
|
||||
document.body.appendChild(hold);
|
||||
block = new SVGTextBlock(hold, {attrs: {'font-size': 12}});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(hold);
|
||||
});
|
||||
|
||||
it('produces a format compatible with SVGTextBlock', () => {
|
||||
const formatted = parser('hello everybody');
|
||||
block.set({formatted});
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.children[0].innerHTML).toEqual('hello everybody');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,20 +1,35 @@
|
|||
define([
|
||||
'core/ArrayUtilities',
|
||||
'./Tokeniser',
|
||||
'./MarkdownParser',
|
||||
'./LabelPatternParser',
|
||||
'./CodeMirrorHints',
|
||||
], (
|
||||
array,
|
||||
Tokeniser,
|
||||
labelPatternParser,
|
||||
CMHints
|
||||
markdownParser,
|
||||
labelPatternParser
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const BLOCK_TYPES = {
|
||||
'if': {type: 'block begin', mode: 'if', skip: []},
|
||||
'else': {type: 'block split', mode: 'else', skip: ['if']},
|
||||
'repeat': {type: 'block begin', mode: 'repeat', skip: []},
|
||||
'if': {
|
||||
type: 'block begin',
|
||||
blockType: 'if',
|
||||
tag: 'if',
|
||||
skip: [],
|
||||
},
|
||||
'else': {
|
||||
type: 'block split',
|
||||
blockType: 'else',
|
||||
tag: 'else',
|
||||
skip: ['if'],
|
||||
},
|
||||
'repeat': {
|
||||
type: 'block begin',
|
||||
blockType: 'repeat',
|
||||
tag: 'repeat',
|
||||
skip: [],
|
||||
},
|
||||
};
|
||||
|
||||
const CONNECT_TYPES = ((() => {
|
||||
|
@ -314,7 +329,8 @@ define([
|
|||
skip = skipOver(line, skip, [':']);
|
||||
return {
|
||||
type: type.type,
|
||||
mode: type.mode,
|
||||
blockType: type.blockType,
|
||||
tag: type.tag,
|
||||
label: joinLabel(line, skip),
|
||||
};
|
||||
},
|
||||
|
@ -345,7 +361,8 @@ define([
|
|||
return {
|
||||
type: 'group begin',
|
||||
agents,
|
||||
mode: 'ref',
|
||||
blockType: 'ref',
|
||||
tag: 'ref',
|
||||
label: def.name,
|
||||
alias: def.alias,
|
||||
};
|
||||
|
@ -480,10 +497,6 @@ define([
|
|||
);
|
||||
}
|
||||
|
||||
getCodeMirrorHints() {
|
||||
return CMHints.getHints;
|
||||
}
|
||||
|
||||
parseLines(lines) {
|
||||
const result = {
|
||||
meta: {
|
||||
|
@ -491,6 +504,7 @@ define([
|
|||
theme: '',
|
||||
terminators: 'none',
|
||||
headers: 'box',
|
||||
textFormatter: markdownParser,
|
||||
},
|
||||
stages: [],
|
||||
};
|
||||
|
|
|
@ -6,26 +6,30 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
const PARSED = {
|
||||
blockBegin: ({
|
||||
ln = jasmine.anything(),
|
||||
mode = jasmine.anything(),
|
||||
blockType = jasmine.anything(),
|
||||
tag = jasmine.anything(),
|
||||
label = jasmine.anything(),
|
||||
} = {}) => {
|
||||
return {
|
||||
type: 'block begin',
|
||||
ln,
|
||||
mode,
|
||||
blockType,
|
||||
tag,
|
||||
label,
|
||||
};
|
||||
},
|
||||
|
||||
blockSplit: ({
|
||||
ln = jasmine.anything(),
|
||||
mode = jasmine.anything(),
|
||||
blockType = jasmine.anything(),
|
||||
tag = jasmine.anything(),
|
||||
label = jasmine.anything(),
|
||||
} = {}) => {
|
||||
return {
|
||||
type: 'block split',
|
||||
ln,
|
||||
mode,
|
||||
blockType,
|
||||
tag,
|
||||
label,
|
||||
};
|
||||
},
|
||||
|
@ -73,6 +77,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
theme: '',
|
||||
terminators: 'none',
|
||||
headers: 'box',
|
||||
textFormatter: jasmine.anything(),
|
||||
},
|
||||
stages: [],
|
||||
});
|
||||
|
@ -98,6 +103,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
expect(parsed.meta.headers).toEqual('bar');
|
||||
});
|
||||
|
||||
it('propagates a function which can be used to format text', () => {
|
||||
const parsed = parser.parse('title foo');
|
||||
expect(parsed.meta.textFormatter).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('reads multiple tokens as one when reading values', () => {
|
||||
const parsed = parser.parse('title foo bar');
|
||||
expect(parsed.meta.title).toEqual('foo bar');
|
||||
|
@ -435,7 +445,8 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
type: 'group begin',
|
||||
ln: jasmine.anything(),
|
||||
agents: [],
|
||||
mode: 'ref',
|
||||
blockType: 'ref',
|
||||
tag: 'ref',
|
||||
label: 'Foo bar',
|
||||
alias: 'baz',
|
||||
},
|
||||
|
@ -446,7 +457,8 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
{name: 'A', alias: '', flags: []},
|
||||
{name: 'B', alias: '', flags: []},
|
||||
],
|
||||
mode: 'ref',
|
||||
blockType: 'ref',
|
||||
tag: 'ref',
|
||||
label: 'Foo bar',
|
||||
alias: 'baz',
|
||||
},
|
||||
|
@ -518,12 +530,24 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
'end\n'
|
||||
);
|
||||
expect(parsed.stages).toEqual([
|
||||
PARSED.blockBegin({mode: 'if', label: 'something happens'}),
|
||||
PARSED.blockBegin({
|
||||
blockType: 'if',
|
||||
tag: 'if',
|
||||
label: 'something happens',
|
||||
}),
|
||||
PARSED.connect(['A', 'B']),
|
||||
PARSED.blockSplit({mode: 'else', label: 'something else'}),
|
||||
PARSED.blockSplit({
|
||||
blockType: 'else',
|
||||
tag: 'else',
|
||||
label: 'something else',
|
||||
}),
|
||||
PARSED.connect(['A', 'C']),
|
||||
PARSED.connect(['C', 'B']),
|
||||
PARSED.blockSplit({mode: 'else', label: ''}),
|
||||
PARSED.blockSplit({
|
||||
blockType: 'else',
|
||||
tag: 'else',
|
||||
label: '',
|
||||
}),
|
||||
PARSED.connect(['A', 'D']),
|
||||
PARSED.blockEnd(),
|
||||
]);
|
||||
|
@ -532,7 +556,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
|
|||
it('converts loop blocks', () => {
|
||||
const parsed = parser.parse('repeat until something');
|
||||
expect(parsed.stages).toEqual([
|
||||
PARSED.blockBegin({mode: 'repeat', label: 'until something'}),
|
||||
PARSED.blockBegin({
|
||||
blockType: 'repeat',
|
||||
tag: 'repeat',
|
||||
label: 'until something',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ define([
|
|||
/* jshint +W072 */
|
||||
'use strict';
|
||||
|
||||
function findExtremes(agentInfos, agentNames) {
|
||||
function findExtremes(agentInfos, agentIDs) {
|
||||
let min = null;
|
||||
let max = null;
|
||||
agentNames.forEach((name) => {
|
||||
const info = agentInfos.get(name);
|
||||
agentIDs.forEach((id) => {
|
||||
const info = agentInfos.get(id);
|
||||
if(min === null || info.index < min.index) {
|
||||
min = info;
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ define([
|
|||
}
|
||||
});
|
||||
return {
|
||||
left: min.label,
|
||||
right: max.label,
|
||||
left: min.id,
|
||||
right: max.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -158,23 +158,23 @@ define([
|
|||
return namespacedName;
|
||||
}
|
||||
|
||||
addSeparation(agentName1, agentName2, dist) {
|
||||
const info1 = this.agentInfos.get(agentName1);
|
||||
const info2 = this.agentInfos.get(agentName2);
|
||||
addSeparation(agentID1, agentID2, dist) {
|
||||
const info1 = this.agentInfos.get(agentID1);
|
||||
const info2 = this.agentInfos.get(agentID2);
|
||||
|
||||
const d1 = info1.separations.get(agentName2) || 0;
|
||||
info1.separations.set(agentName2, Math.max(d1, dist));
|
||||
const d1 = info1.separations.get(agentID2) || 0;
|
||||
info1.separations.set(agentID2, Math.max(d1, dist));
|
||||
|
||||
const d2 = info2.separations.get(agentName1) || 0;
|
||||
info2.separations.set(agentName1, Math.max(d2, dist));
|
||||
const d2 = info2.separations.get(agentID1) || 0;
|
||||
info2.separations.set(agentID1, Math.max(d2, dist));
|
||||
}
|
||||
|
||||
separationStage(stage) {
|
||||
const agentSpaces = new Map();
|
||||
const agentNames = this.visibleAgents.slice();
|
||||
const agentIDs = this.visibleAgentIDs.slice();
|
||||
|
||||
const addSpacing = (agentName, {left, right}) => {
|
||||
const current = agentSpaces.get(agentName);
|
||||
const addSpacing = (agentID, {left, right}) => {
|
||||
const current = agentSpaces.get(agentID);
|
||||
current.left = Math.max(current.left, left);
|
||||
current.right = Math.max(current.right, right);
|
||||
};
|
||||
|
@ -182,12 +182,12 @@ define([
|
|||
this.agentInfos.forEach((agentInfo) => {
|
||||
const rad = agentInfo.currentRad;
|
||||
agentInfo.currentMaxRad = rad;
|
||||
agentSpaces.set(agentInfo.label, {left: rad, right: rad});
|
||||
agentSpaces.set(agentInfo.id, {left: rad, right: rad});
|
||||
});
|
||||
const env = {
|
||||
theme: this.theme,
|
||||
agentInfos: this.agentInfos,
|
||||
visibleAgents: this.visibleAgents,
|
||||
visibleAgentIDs: this.visibleAgentIDs,
|
||||
textSizer: this.sizer,
|
||||
addSpacing,
|
||||
addSeparation: this.addSeparation,
|
||||
|
@ -200,33 +200,33 @@ define([
|
|||
}
|
||||
component.separationPre(stage, env);
|
||||
component.separation(stage, env);
|
||||
array.mergeSets(agentNames, this.visibleAgents);
|
||||
array.mergeSets(agentIDs, this.visibleAgentIDs);
|
||||
|
||||
agentNames.forEach((agentNameR) => {
|
||||
const infoR = this.agentInfos.get(agentNameR);
|
||||
const sepR = agentSpaces.get(agentNameR);
|
||||
agentIDs.forEach((agentIDR) => {
|
||||
const infoR = this.agentInfos.get(agentIDR);
|
||||
const sepR = agentSpaces.get(agentIDR);
|
||||
infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right);
|
||||
infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left);
|
||||
agentNames.forEach((agentNameL) => {
|
||||
const infoL = this.agentInfos.get(agentNameL);
|
||||
agentIDs.forEach((agentIDL) => {
|
||||
const infoL = this.agentInfos.get(agentIDL);
|
||||
if(infoL.index >= infoR.index) {
|
||||
return;
|
||||
}
|
||||
const sepL = agentSpaces.get(agentNameL);
|
||||
const sepL = agentSpaces.get(agentIDL);
|
||||
this.addSeparation(
|
||||
agentNameR,
|
||||
agentNameL,
|
||||
agentIDR,
|
||||
agentIDL,
|
||||
sepR.left + sepL.right + this.theme.agentMargin
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
checkAgentRange(agentNames, topY = 0) {
|
||||
if(agentNames.length === 0) {
|
||||
checkAgentRange(agentIDs, topY = 0) {
|
||||
if(agentIDs.length === 0) {
|
||||
return topY;
|
||||
}
|
||||
const {left, right} = findExtremes(this.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(this.agentInfos, agentIDs);
|
||||
const leftX = this.agentInfos.get(left).x;
|
||||
const rightX = this.agentInfos.get(right).x;
|
||||
let baseY = topY;
|
||||
|
@ -238,11 +238,11 @@ define([
|
|||
return baseY;
|
||||
}
|
||||
|
||||
markAgentRange(agentNames, y) {
|
||||
if(agentNames.length === 0) {
|
||||
markAgentRange(agentIDs, y) {
|
||||
if(agentIDs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const {left, right} = findExtremes(this.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(this.agentInfos, agentIDs);
|
||||
const leftX = this.agentInfos.get(left).x;
|
||||
const rightX = this.agentInfos.get(right).x;
|
||||
this.agentInfos.forEach((agentInfo) => {
|
||||
|
@ -293,10 +293,10 @@ define([
|
|||
};
|
||||
const component = this.components.get(stage.type);
|
||||
const result = component.renderPre(stage, envPre);
|
||||
const {topShift, agentNames, asynchronousY} =
|
||||
const {topShift, agentIDs, asynchronousY} =
|
||||
BaseComponent.cleanRenderPreResult(result, this.currentY);
|
||||
|
||||
const topY = this.checkAgentRange(agentNames, asynchronousY);
|
||||
const topY = this.checkAgentRange(agentIDs, asynchronousY);
|
||||
|
||||
const eventOut = () => {
|
||||
this.trigger('mouseout');
|
||||
|
@ -332,8 +332,8 @@ define([
|
|||
textSizer: this.sizer,
|
||||
SVGTextBlockClass: this.SVGTextBlockClass,
|
||||
state: this.state,
|
||||
drawAgentLine: (agentName, toY, andStop = false) => {
|
||||
const agentInfo = this.agentInfos.get(agentName);
|
||||
drawAgentLine: (agentID, toY, andStop = false) => {
|
||||
const agentInfo = this.agentInfos.get(agentID);
|
||||
this.drawAgentLine(agentInfo, toY);
|
||||
agentInfo.latestYStart = andStop ? null : toY;
|
||||
},
|
||||
|
@ -343,7 +343,7 @@ define([
|
|||
};
|
||||
|
||||
const bottomY = Math.max(topY, component.render(stage, env) || 0);
|
||||
this.markAgentRange(agentNames, bottomY);
|
||||
this.markAgentRange(agentIDs, bottomY);
|
||||
|
||||
this.currentY = bottomY;
|
||||
}
|
||||
|
@ -379,7 +379,7 @@ define([
|
|||
agentInfo.x = currentX;
|
||||
});
|
||||
|
||||
this.agentInfos.forEach(({label, x, maxRPad, maxLPad}) => {
|
||||
this.agentInfos.forEach(({x, maxRPad, maxLPad}) => {
|
||||
this.minX = Math.min(this.minX, x - maxLPad);
|
||||
this.maxX = Math.max(this.maxX, x + maxRPad);
|
||||
});
|
||||
|
@ -388,8 +388,9 @@ define([
|
|||
buildAgentInfos(agents, stages) {
|
||||
this.agentInfos = new Map();
|
||||
agents.forEach((agent, index) => {
|
||||
this.agentInfos.set(agent.name, {
|
||||
label: agent.name,
|
||||
this.agentInfos.set(agent.id, {
|
||||
id: agent.id,
|
||||
formattedLabel: agent.formattedLabel,
|
||||
anchorRight: agent.anchorRight,
|
||||
index,
|
||||
x: null,
|
||||
|
@ -403,7 +404,7 @@ define([
|
|||
});
|
||||
});
|
||||
|
||||
this.visibleAgents = ['[', ']'];
|
||||
this.visibleAgentIDs = ['[', ']'];
|
||||
stages.forEach(this.separationStage);
|
||||
|
||||
this.positionAgents();
|
||||
|
@ -497,7 +498,7 @@ define([
|
|||
|
||||
this.title.set({
|
||||
attrs: this.theme.titleAttrs,
|
||||
text: sequence.meta.title,
|
||||
formatted: sequence.meta.title,
|
||||
});
|
||||
|
||||
this.minX = 0;
|
||||
|
@ -534,8 +535,8 @@ define([
|
|||
return this.themes.get('');
|
||||
}
|
||||
|
||||
getAgentX(name) {
|
||||
return this.agentInfos.get(name).x;
|
||||
getAgentX(id) {
|
||||
return this.agentInfos.get(id).x;
|
||||
}
|
||||
|
||||
svg() {
|
||||
|
|
|
@ -26,10 +26,10 @@ defineDescribe('Sequence Renderer', [
|
|||
});
|
||||
|
||||
const GENERATED = {
|
||||
connect: (agentNames, label = '') => {
|
||||
connect: (agentIDs, label = []) => {
|
||||
return {
|
||||
type: 'connect',
|
||||
agentNames,
|
||||
agentIDs,
|
||||
label,
|
||||
options: {
|
||||
line: 'solid',
|
||||
|
@ -40,15 +40,35 @@ defineDescribe('Sequence Renderer', [
|
|||
},
|
||||
};
|
||||
|
||||
function format(text) {
|
||||
if(!text) {
|
||||
return [];
|
||||
}
|
||||
return [[{text}]];
|
||||
}
|
||||
|
||||
describe('.render', () => {
|
||||
it('populates the SVG with content', () => {
|
||||
renderer.render({
|
||||
meta: {title: 'Title'},
|
||||
meta: {title: format('Title')},
|
||||
agents: [
|
||||
{name: '[', anchorRight: true},
|
||||
{name: 'Col 1', anchorRight: false},
|
||||
{name: 'Col 2', anchorRight: false},
|
||||
{name: ']', anchorRight: false},
|
||||
{
|
||||
id: '[',
|
||||
formattedLabel: null,
|
||||
anchorRight: true,
|
||||
}, {
|
||||
id: 'Col 1',
|
||||
formattedLabel: format('Col 1!'),
|
||||
anchorRight: false,
|
||||
}, {
|
||||
id: 'Col 2',
|
||||
formattedLabel: format('Col 2!'),
|
||||
anchorRight: false,
|
||||
}, {
|
||||
id: ']',
|
||||
formattedLabel: null,
|
||||
anchorRight: false,
|
||||
},
|
||||
],
|
||||
stages: [],
|
||||
});
|
||||
|
@ -63,17 +83,17 @@ defineDescribe('Sequence Renderer', [
|
|||
*/
|
||||
|
||||
renderer.render({
|
||||
meta: {title: ''},
|
||||
meta: {title: []},
|
||||
agents: [
|
||||
{name: '[', anchorRight: true},
|
||||
{name: 'A', anchorRight: false},
|
||||
{name: 'B', anchorRight: false},
|
||||
{name: ']', anchorRight: false},
|
||||
{id: '[', formattedLabel: null, anchorRight: true},
|
||||
{id: 'A', formattedLabel: format('A!'), anchorRight: false},
|
||||
{id: 'B', formattedLabel: format('B!'), anchorRight: false},
|
||||
{id: ']', formattedLabel: null, anchorRight: false},
|
||||
],
|
||||
stages: [
|
||||
{type: 'agent begin', agentNames: ['A', 'B'], mode: 'box'},
|
||||
{type: 'agent begin', agentIDs: ['A', 'B'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'B']),
|
||||
{type: 'agent end', agentNames: ['A', 'B'], mode: 'none'},
|
||||
{type: 'agent end', agentIDs: ['A', 'B'], mode: 'none'},
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -93,18 +113,18 @@ defineDescribe('Sequence Renderer', [
|
|||
*/
|
||||
|
||||
renderer.render({
|
||||
meta: {title: ''},
|
||||
meta: {title: []},
|
||||
agents: [
|
||||
{name: '[', anchorRight: true},
|
||||
{name: 'A', anchorRight: false},
|
||||
{name: 'B', anchorRight: false},
|
||||
{name: 'C', anchorRight: false},
|
||||
{name: ']', anchorRight: false},
|
||||
{id: '[', formattedLabel: null, anchorRight: true},
|
||||
{id: 'A', formattedLabel: format('A!'), anchorRight: false},
|
||||
{id: 'B', formattedLabel: format('B!'), anchorRight: false},
|
||||
{id: 'C', formattedLabel: format('C!'), anchorRight: false},
|
||||
{id: ']', formattedLabel: null, anchorRight: false},
|
||||
],
|
||||
stages: [
|
||||
{
|
||||
type: 'agent begin',
|
||||
agentNames: ['A', 'B', 'C'],
|
||||
agentIDs: ['A', 'B', 'C'],
|
||||
mode: 'box',
|
||||
},
|
||||
GENERATED.connect(['[', 'A']),
|
||||
|
@ -113,7 +133,7 @@ defineDescribe('Sequence Renderer', [
|
|||
GENERATED.connect(['C', ']']),
|
||||
{
|
||||
type: 'agent end',
|
||||
agentNames: ['A', 'B', 'C'],
|
||||
agentIDs: ['A', 'B', 'C'],
|
||||
mode: 'none',
|
||||
},
|
||||
],
|
||||
|
@ -142,25 +162,25 @@ defineDescribe('Sequence Renderer', [
|
|||
*/
|
||||
|
||||
renderer.render({
|
||||
meta: {title: ''},
|
||||
meta: {title: []},
|
||||
agents: [
|
||||
{name: '[', anchorRight: true},
|
||||
{name: 'A', anchorRight: false},
|
||||
{name: 'B', anchorRight: false},
|
||||
{name: 'C', anchorRight: false},
|
||||
{name: 'D', anchorRight: false},
|
||||
{name: ']', anchorRight: false},
|
||||
{id: '[', formattedLabel: null, anchorRight: true},
|
||||
{id: 'A', formattedLabel: format('A!'), anchorRight: false},
|
||||
{id: 'B', formattedLabel: format('B!'), anchorRight: false},
|
||||
{id: 'C', formattedLabel: format('C!'), anchorRight: false},
|
||||
{id: 'D', formattedLabel: format('D!'), anchorRight: false},
|
||||
{id: ']', formattedLabel: null, anchorRight: false},
|
||||
],
|
||||
stages: [
|
||||
{type: 'agent begin', agentNames: ['A', 'B'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'B'], 'short'),
|
||||
{type: 'agent end', agentNames: ['B'], mode: 'cross'},
|
||||
{type: 'agent begin', agentNames: ['C'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'C'], 'long description here'),
|
||||
{type: 'agent end', agentNames: ['C'], mode: 'cross'},
|
||||
{type: 'agent begin', agentNames: ['D'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'D'], 'short again'),
|
||||
{type: 'agent end', agentNames: ['A', 'D'], mode: 'cross'},
|
||||
{type: 'agent begin', agentIDs: ['A', 'B'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'B'], format('short')),
|
||||
{type: 'agent end', agentIDs: ['B'], mode: 'cross'},
|
||||
{type: 'agent begin', agentIDs: ['C'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'C'], format('long description')),
|
||||
{type: 'agent end', agentIDs: ['C'], mode: 'cross'},
|
||||
{type: 'agent begin', agentIDs: ['D'], mode: 'box'},
|
||||
GENERATED.connect(['A', 'D'], format('short again')),
|
||||
{type: 'agent end', agentIDs: ['A', 'D'], mode: 'cross'},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ define([
|
|||
'./Generator',
|
||||
'./Renderer',
|
||||
'./Exporter',
|
||||
'./CodeMirrorHints',
|
||||
'./themes/BaseTheme',
|
||||
'./themes/Basic',
|
||||
'./themes/Monospace',
|
||||
|
@ -16,6 +17,7 @@ define([
|
|||
Generator,
|
||||
Renderer,
|
||||
Exporter,
|
||||
CMHints,
|
||||
BaseTheme,
|
||||
BasicTheme,
|
||||
MonospaceTheme,
|
||||
|
@ -36,14 +38,13 @@ define([
|
|||
const SharedParser = new Parser();
|
||||
const SharedGenerator = new Generator();
|
||||
const CMMode = SharedParser.getCodeMirrorMode();
|
||||
const CMHints = SharedParser.getCodeMirrorHints();
|
||||
|
||||
function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') {
|
||||
if(!CodeMirror) {
|
||||
CodeMirror = window.CodeMirror;
|
||||
}
|
||||
CodeMirror.defineMode(modeName, () => CMMode);
|
||||
CodeMirror.registerHelper('hint', modeName, CMHints);
|
||||
CodeMirror.registerHelper('hint', modeName, CMHints.getHints);
|
||||
}
|
||||
|
||||
function addTheme(theme) {
|
||||
|
|
|
@ -12,10 +12,10 @@ define([
|
|||
'use strict';
|
||||
|
||||
class CapBox {
|
||||
separation({label}, env) {
|
||||
separation({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
|
@ -27,20 +27,20 @@ define([
|
|||
};
|
||||
}
|
||||
|
||||
topShift({label}, env) {
|
||||
topShift({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const height = (
|
||||
env.textSizer.measureHeight(config.labelAttrs, label) +
|
||||
env.textSizer.measureHeight(config.labelAttrs, formattedLabel) +
|
||||
config.padding.top +
|
||||
config.padding.bottom
|
||||
);
|
||||
return Math.max(0, height - config.arrowBottom);
|
||||
}
|
||||
|
||||
render(y, {x, label}, env) {
|
||||
render(y, {x, formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const clickable = env.makeRegion();
|
||||
const {width, height} = SVGShapes.renderBoxedText(label, {
|
||||
const {width, height} = SVGShapes.renderBoxedText(formattedLabel, {
|
||||
x,
|
||||
y,
|
||||
padding: config.padding,
|
||||
|
@ -105,10 +105,10 @@ define([
|
|||
}
|
||||
|
||||
class CapBar {
|
||||
separation({label}, env) {
|
||||
separation({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
|
@ -125,17 +125,17 @@ define([
|
|||
return config.height / 2;
|
||||
}
|
||||
|
||||
render(y, {x, label}, env) {
|
||||
const configB = env.theme.agentCap.box;
|
||||
const config = env.theme.agentCap.bar;
|
||||
render(y, {x, formattedLabel}, env) {
|
||||
const boxCfg = env.theme.agentCap.box;
|
||||
const barCfg = env.theme.agentCap.bar;
|
||||
const width = (
|
||||
env.textSizer.measure(configB.labelAttrs, label).width +
|
||||
configB.padding.left +
|
||||
configB.padding.right
|
||||
env.textSizer.measure(boxCfg.labelAttrs, formattedLabel).width +
|
||||
boxCfg.padding.left +
|
||||
boxCfg.padding.right
|
||||
);
|
||||
const height = config.height;
|
||||
const height = barCfg.height;
|
||||
|
||||
env.shapeLayer.appendChild(config.render({
|
||||
env.shapeLayer.appendChild(barCfg.render({
|
||||
x: x - width / 2,
|
||||
y,
|
||||
width,
|
||||
|
@ -172,7 +172,7 @@ define([
|
|||
return isBegin ? config.height : 0;
|
||||
}
|
||||
|
||||
render(y, {x, label}, env, isBegin) {
|
||||
render(y, {x}, env, isBegin) {
|
||||
const config = env.theme.agentCap.fade;
|
||||
const ratio = config.height / (config.height + config.extend);
|
||||
|
||||
|
@ -266,12 +266,12 @@ define([
|
|||
this.begin = begin;
|
||||
}
|
||||
|
||||
separationPre({mode, agentNames}, env) {
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
separationPre({mode, agentIDs}, env) {
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
const cap = AGENT_CAPS[mode];
|
||||
const sep = cap.separation(agentInfo, env, this.begin);
|
||||
env.addSpacing(name, sep);
|
||||
env.addSpacing(id, sep);
|
||||
agentInfo.currentMaxRad = Math.max(
|
||||
agentInfo.currentMaxRad,
|
||||
sep.radius
|
||||
|
@ -279,18 +279,18 @@ define([
|
|||
});
|
||||
}
|
||||
|
||||
separation({mode, agentNames}, env) {
|
||||
separation({mode, agentIDs}, env) {
|
||||
if(this.begin) {
|
||||
array.mergeSets(env.visibleAgents, agentNames);
|
||||
array.mergeSets(env.visibleAgentIDs, agentIDs);
|
||||
} else {
|
||||
array.removeAll(env.visibleAgents, agentNames);
|
||||
array.removeAll(env.visibleAgentIDs, agentIDs);
|
||||
}
|
||||
}
|
||||
|
||||
renderPre({mode, agentNames}, env) {
|
||||
renderPre({mode, agentIDs}, env) {
|
||||
let maxTopShift = 0;
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
const cap = AGENT_CAPS[mode];
|
||||
const topShift = cap.topShift(agentInfo, env, this.begin);
|
||||
maxTopShift = Math.max(maxTopShift, topShift);
|
||||
|
@ -299,15 +299,15 @@ define([
|
|||
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
|
||||
});
|
||||
return {
|
||||
agentNames,
|
||||
agentIDs,
|
||||
topShift: maxTopShift,
|
||||
};
|
||||
}
|
||||
|
||||
render({mode, agentNames}, env) {
|
||||
render({mode, agentIDs}, env) {
|
||||
let maxEnd = 0;
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
const cap = AGENT_CAPS[mode];
|
||||
const topShift = cap.topShift(agentInfo, env, this.begin);
|
||||
const y0 = env.primaryY - topShift;
|
||||
|
@ -319,9 +319,9 @@ define([
|
|||
);
|
||||
maxEnd = Math.max(maxEnd, y0 + shifts.height);
|
||||
if(this.begin) {
|
||||
env.drawAgentLine(name, y0 + shifts.lineBottom);
|
||||
env.drawAgentLine(id, y0 + shifts.lineBottom);
|
||||
} else {
|
||||
env.drawAgentLine(name, y0 + shifts.lineTop, true);
|
||||
env.drawAgentLine(id, y0 + shifts.lineTop, true);
|
||||
}
|
||||
});
|
||||
return maxEnd + env.theme.actionMargin;
|
||||
|
|
|
@ -6,28 +6,28 @@ define(['./BaseComponent'], (BaseComponent) => {
|
|||
return highlighted ? env.theme.agentLineHighlightRadius : 0;
|
||||
}
|
||||
|
||||
separationPre({agentNames, highlighted}, env) {
|
||||
separationPre({agentIDs, highlighted}, env) {
|
||||
const r = this.radius(highlighted, env);
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
agentInfo.currentRad = r;
|
||||
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
|
||||
});
|
||||
}
|
||||
|
||||
renderPre({agentNames, highlighted}, env) {
|
||||
renderPre({agentIDs, highlighted}, env) {
|
||||
const r = this.radius(highlighted, env);
|
||||
agentNames.forEach((name) => {
|
||||
const agentInfo = env.agentInfos.get(name);
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
|
||||
});
|
||||
}
|
||||
|
||||
render({agentNames, highlighted}, env) {
|
||||
render({agentIDs, highlighted}, env) {
|
||||
const r = this.radius(highlighted, env);
|
||||
agentNames.forEach((name) => {
|
||||
env.drawAgentLine(name, env.primaryY);
|
||||
env.agentInfos.get(name).currentRad = r;
|
||||
agentIDs.forEach((id) => {
|
||||
env.drawAgentLine(id, env.primaryY);
|
||||
env.agentInfos.get(id).currentRad = r;
|
||||
});
|
||||
return env.primaryY + env.theme.actionMargin;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ defineDescribe('AgentHighlight', [
|
|||
theme,
|
||||
agentInfos,
|
||||
};
|
||||
highlight.separationPre({agentNames: ['foo'], highlighted: true}, env);
|
||||
highlight.separationPre({agentIDs: ['foo'], highlighted: true}, env);
|
||||
expect(agentInfo.currentRad).toEqual(2);
|
||||
expect(agentInfo.currentMaxRad).toEqual(2);
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ defineDescribe('AgentHighlight', [
|
|||
theme,
|
||||
agentInfos,
|
||||
};
|
||||
highlight.separationPre({agentNames: ['foo'], highlighted: true}, env);
|
||||
highlight.separationPre({agentIDs: ['foo'], highlighted: true}, env);
|
||||
expect(agentInfo.currentRad).toEqual(2);
|
||||
expect(agentInfo.currentMaxRad).toEqual(3);
|
||||
});
|
||||
|
@ -54,7 +54,7 @@ defineDescribe('AgentHighlight', [
|
|||
theme,
|
||||
agentInfos,
|
||||
};
|
||||
highlight.separationPre({agentNames: ['foo'], highlighted: false}, env);
|
||||
highlight.separationPre({agentIDs: ['foo'], highlighted: false}, env);
|
||||
expect(agentInfo.currentRad).toEqual(0);
|
||||
expect(agentInfo.currentMaxRad).toEqual(1);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ define(() => {
|
|||
separationPre(/*stage, {
|
||||
theme,
|
||||
agentInfos,
|
||||
visibleAgents,
|
||||
visibleAgentIDs,
|
||||
textSizer,
|
||||
addSpacing,
|
||||
addSeparation,
|
||||
|
@ -24,7 +24,7 @@ define(() => {
|
|||
separation(/*stage, {
|
||||
theme,
|
||||
agentInfos,
|
||||
visibleAgents,
|
||||
visibleAgentIDs,
|
||||
textSizer,
|
||||
addSpacing,
|
||||
addSeparation,
|
||||
|
@ -40,7 +40,7 @@ define(() => {
|
|||
state,
|
||||
components,
|
||||
}*/) {
|
||||
// return {topShift, agentNames, asynchronousY}
|
||||
// return {topShift, agentIDs, asynchronousY}
|
||||
}
|
||||
|
||||
render(/*stage, {
|
||||
|
@ -64,12 +64,12 @@ define(() => {
|
|||
|
||||
BaseComponent.cleanRenderPreResult = ({
|
||||
topShift = 0,
|
||||
agentNames = [],
|
||||
agentIDs = [],
|
||||
asynchronousY = null,
|
||||
} = {}, currentY = null) => {
|
||||
return {
|
||||
topShift,
|
||||
agentNames,
|
||||
agentIDs,
|
||||
asynchronousY: (asynchronousY !== null) ? asynchronousY : currentY,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,13 +12,13 @@ define([
|
|||
'use strict';
|
||||
|
||||
class BlockSplit extends BaseComponent {
|
||||
separation({left, right, mode, label}, env) {
|
||||
separation({left, right, tag, label}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.mode).section;
|
||||
const config = env.theme.getBlock(blockInfo.type).section;
|
||||
const width = (
|
||||
env.textSizer.measure(config.mode.labelAttrs, mode).width +
|
||||
config.mode.padding.left +
|
||||
config.mode.padding.right +
|
||||
env.textSizer.measure(config.tag.labelAttrs, tag).width +
|
||||
config.tag.padding.left +
|
||||
config.tag.padding.right +
|
||||
env.textSizer.measure(config.label.labelAttrs, label).width +
|
||||
config.label.padding.left +
|
||||
config.label.padding.right
|
||||
|
@ -28,13 +28,13 @@ define([
|
|||
|
||||
renderPre({left, right}) {
|
||||
return {
|
||||
agentNames: [left, right],
|
||||
agentIDs: [left, right],
|
||||
};
|
||||
}
|
||||
|
||||
render({left, right, mode, label}, env, first = false) {
|
||||
render({left, right, tag, label}, env, first = false) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.mode);
|
||||
const config = env.theme.getBlock(blockInfo.type);
|
||||
const agentInfoL = env.agentInfos.get(left);
|
||||
const agentInfoR = env.agentInfos.get(right);
|
||||
|
||||
|
@ -46,20 +46,20 @@ define([
|
|||
|
||||
const clickable = env.makeRegion();
|
||||
|
||||
const modeRender = SVGShapes.renderBoxedText(mode, {
|
||||
const tagRender = SVGShapes.renderBoxedText(tag, {
|
||||
x: agentInfoL.x,
|
||||
y,
|
||||
padding: config.section.mode.padding,
|
||||
boxAttrs: config.section.mode.boxAttrs,
|
||||
boxRenderer: config.section.mode.boxRenderer,
|
||||
labelAttrs: config.section.mode.labelAttrs,
|
||||
padding: config.section.tag.padding,
|
||||
boxAttrs: config.section.tag.boxAttrs,
|
||||
boxRenderer: config.section.tag.boxRenderer,
|
||||
labelAttrs: config.section.tag.labelAttrs,
|
||||
boxLayer: blockInfo.hold,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
const labelRender = SVGShapes.renderBoxedText(label, {
|
||||
x: agentInfoL.x + modeRender.width,
|
||||
x: agentInfoL.x + tagRender.width,
|
||||
y,
|
||||
padding: config.section.label.padding,
|
||||
boxAttrs: {'fill': '#000000'},
|
||||
|
@ -69,7 +69,7 @@ define([
|
|||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
const labelHeight = Math.max(modeRender.height, labelRender.height);
|
||||
const labelHeight = Math.max(tagRender.height, labelRender.height);
|
||||
|
||||
clickable.insertBefore(svg.make('rect', {
|
||||
'x': agentInfoL.x,
|
||||
|
@ -102,11 +102,13 @@ define([
|
|||
}
|
||||
|
||||
storeBlockInfo(stage, env) {
|
||||
env.state.blocks.set(stage.left, {
|
||||
mode: stage.mode,
|
||||
const blockInfo = {
|
||||
type: stage.blockType,
|
||||
hold: null,
|
||||
startY: null,
|
||||
});
|
||||
};
|
||||
env.state.blocks.set(stage.left, blockInfo);
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
separationPre(stage, env) {
|
||||
|
@ -114,17 +116,17 @@ define([
|
|||
}
|
||||
|
||||
separation(stage, env) {
|
||||
array.mergeSets(env.visibleAgents, [stage.left, stage.right]);
|
||||
array.mergeSets(env.visibleAgentIDs, [stage.left, stage.right]);
|
||||
super.separation(stage, env);
|
||||
}
|
||||
|
||||
renderPre(stage, env) {
|
||||
this.storeBlockInfo(stage, env);
|
||||
const blockInfo = this.storeBlockInfo(stage, env);
|
||||
|
||||
const config = env.theme.getBlock(stage.mode);
|
||||
const config = env.theme.getBlock(blockInfo.type);
|
||||
|
||||
return {
|
||||
agentNames: [stage.left, stage.right],
|
||||
agentIDs: [stage.left, stage.right],
|
||||
topShift: config.margin.top,
|
||||
};
|
||||
}
|
||||
|
@ -143,22 +145,22 @@ define([
|
|||
|
||||
class BlockEnd extends BaseComponent {
|
||||
separation({left, right}, env) {
|
||||
array.removeAll(env.visibleAgents, [left, right]);
|
||||
array.removeAll(env.visibleAgentIDs, [left, right]);
|
||||
}
|
||||
|
||||
renderPre({left, right}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.mode);
|
||||
const config = env.theme.getBlock(blockInfo.type);
|
||||
|
||||
return {
|
||||
agentNames: [left, right],
|
||||
agentIDs: [left, right],
|
||||
topShift: config.section.padding.bottom,
|
||||
};
|
||||
}
|
||||
|
||||
render({left, right}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.mode);
|
||||
const config = env.theme.getBlock(blockInfo.type);
|
||||
const agentInfoL = env.agentInfos.get(left);
|
||||
const agentInfoR = env.agentInfos.get(right);
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ define([
|
|||
];
|
||||
|
||||
class Connect extends BaseComponent {
|
||||
separation({label, agentNames, options}, env) {
|
||||
separation({label, agentIDs, options}, env) {
|
||||
const config = env.theme.connect;
|
||||
|
||||
const lArrow = ARROWHEADS[options.left];
|
||||
|
@ -90,9 +90,9 @@ define([
|
|||
labelWidth += config.label.padding * 2;
|
||||
}
|
||||
|
||||
const info1 = env.agentInfos.get(agentNames[0]);
|
||||
if(agentNames[0] === agentNames[1]) {
|
||||
env.addSpacing(agentNames[0], {
|
||||
const info1 = env.agentInfos.get(agentIDs[0]);
|
||||
if(agentIDs[0] === agentIDs[1]) {
|
||||
env.addSpacing(agentIDs[0], {
|
||||
left: 0,
|
||||
right: (
|
||||
info1.currentMaxRad +
|
||||
|
@ -104,10 +104,10 @@ define([
|
|||
),
|
||||
});
|
||||
} else {
|
||||
const info2 = env.agentInfos.get(agentNames[1]);
|
||||
const info2 = env.agentInfos.get(agentIDs[1]);
|
||||
env.addSeparation(
|
||||
agentNames[0],
|
||||
agentNames[1],
|
||||
agentIDs[0],
|
||||
agentIDs[1],
|
||||
|
||||
info1.currentMaxRad +
|
||||
info2.currentMaxRad +
|
||||
|
@ -120,10 +120,10 @@ define([
|
|||
}
|
||||
}
|
||||
|
||||
renderSelfConnect({label, agentNames, options}, env) {
|
||||
renderSelfConnect({label, agentIDs, options}, env) {
|
||||
/* jshint -W071 */ // TODO: find appropriate abstractions
|
||||
const config = env.theme.connect;
|
||||
const from = env.agentInfos.get(agentNames[0]);
|
||||
const from = env.agentInfos.get(agentIDs[0]);
|
||||
|
||||
const lArrow = ARROWHEADS[options.left];
|
||||
const rArrow = ARROWHEADS[options.right];
|
||||
|
@ -195,10 +195,10 @@ define([
|
|||
);
|
||||
}
|
||||
|
||||
renderSimpleConnect({label, agentNames, options}, env) {
|
||||
renderSimpleConnect({label, agentIDs, options}, env) {
|
||||
const config = env.theme.connect;
|
||||
const from = env.agentInfos.get(agentNames[0]);
|
||||
const to = env.agentInfos.get(agentNames[1]);
|
||||
const from = env.agentInfos.get(agentIDs[0]);
|
||||
const to = env.agentInfos.get(agentIDs[1]);
|
||||
|
||||
const lArrow = ARROWHEADS[options.left];
|
||||
const rArrow = ARROWHEADS[options.right];
|
||||
|
@ -260,7 +260,7 @@ define([
|
|||
);
|
||||
}
|
||||
|
||||
renderPre({label, agentNames, options}, env) {
|
||||
renderPre({label, agentIDs, options}, env) {
|
||||
const config = env.theme.connect;
|
||||
|
||||
const lArrow = ARROWHEADS[options.left];
|
||||
|
@ -273,18 +273,18 @@ define([
|
|||
);
|
||||
|
||||
let arrowH = lArrow.height(env.theme);
|
||||
if(agentNames[0] !== agentNames[1]) {
|
||||
if(agentIDs[0] !== agentIDs[1]) {
|
||||
arrowH = Math.max(arrowH, rArrow.height(env.theme));
|
||||
}
|
||||
|
||||
return {
|
||||
agentNames,
|
||||
agentIDs,
|
||||
topShift: Math.max(arrowH / 2, height),
|
||||
};
|
||||
}
|
||||
|
||||
render(stage, env) {
|
||||
if(stage.agentNames[0] === stage.agentNames[1]) {
|
||||
if(stage.agentIDs[0] === stage.agentIDs[1]) {
|
||||
return this.renderSelfConnect(stage, env);
|
||||
} else {
|
||||
return this.renderSimpleConnect(stage, env);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
||||
'use strict';
|
||||
|
||||
function findExtremes(agentInfos, agentNames) {
|
||||
function findExtremes(agentInfos, agentIDs) {
|
||||
let min = null;
|
||||
let max = null;
|
||||
agentNames.forEach((name) => {
|
||||
const info = agentInfos.get(name);
|
||||
agentIDs.forEach((id) => {
|
||||
const info = agentInfos.get(id);
|
||||
if(min === null || info.index < min.index) {
|
||||
min = info;
|
||||
}
|
||||
|
@ -14,14 +14,14 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}
|
||||
});
|
||||
return {
|
||||
left: min.label,
|
||||
right: max.label,
|
||||
left: min.id,
|
||||
right: max.id,
|
||||
};
|
||||
}
|
||||
|
||||
class NoteComponent extends BaseComponent {
|
||||
renderPre({agentNames}) {
|
||||
return {agentNames};
|
||||
renderPre({agentIDs}) {
|
||||
return {agentIDs};
|
||||
}
|
||||
|
||||
renderNote({
|
||||
|
@ -39,7 +39,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
const y = env.topY + config.margin.top + config.padding.top;
|
||||
const labelNode = new env.SVGTextBlockClass(clickable, {
|
||||
attrs: config.labelAttrs,
|
||||
text: label,
|
||||
formatted: label,
|
||||
y,
|
||||
});
|
||||
|
||||
|
@ -105,7 +105,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}
|
||||
|
||||
class NoteOver extends NoteComponent {
|
||||
separation({agentNames, mode, label}, env) {
|
||||
separation({agentIDs, mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
|
@ -113,7 +113,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
config.padding.right
|
||||
);
|
||||
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentIDs);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
if(infoL !== infoR) {
|
||||
|
@ -132,10 +132,10 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}
|
||||
}
|
||||
|
||||
render({agentNames, mode, label}, env) {
|
||||
render({agentIDs, mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentIDs);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
if(infoL !== infoR) {
|
||||
|
@ -164,9 +164,9 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
this.isRight = isRight;
|
||||
}
|
||||
|
||||
separation({agentNames, mode, label}, env) {
|
||||
separation({agentIDs, mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentIDs);
|
||||
const width = (
|
||||
env.textSizer.measure(config.labelAttrs, label).width +
|
||||
config.padding.left +
|
||||
|
@ -190,9 +190,9 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}
|
||||
}
|
||||
|
||||
render({agentNames, mode, label}, env) {
|
||||
render({agentIDs, mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentIDs);
|
||||
if(this.isRight) {
|
||||
const info = env.agentInfos.get(right);
|
||||
const x0 = info.x + info.currentMaxRad + config.margin.left;
|
||||
|
@ -216,9 +216,9 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}
|
||||
|
||||
class NoteBetween extends NoteComponent {
|
||||
separation({agentNames, mode, label}, env) {
|
||||
separation({agentIDs, mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
const {left, right} = findExtremes(env.agentInfos, agentIDs);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
|
||||
|
@ -236,8 +236,8 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
);
|
||||
}
|
||||
|
||||
render({agentNames, mode, label}, env) {
|
||||
const {left, right} = findExtremes(env.agentInfos, agentNames);
|
||||
render({agentIDs, mode, label}, env) {
|
||||
const {left, right} = findExtremes(env.agentInfos, agentIDs);
|
||||
const infoL = env.agentInfos.get(left);
|
||||
const infoR = env.agentInfos.get(right);
|
||||
const xMid = (
|
||||
|
|
|
@ -18,10 +18,10 @@ define([
|
|||
}
|
||||
|
||||
function mergeResults(a, b) {
|
||||
array.mergeSets(a.agentNames, b.agentNames);
|
||||
array.mergeSets(a.agentIDs, b.agentIDs);
|
||||
return {
|
||||
topShift: Math.max(a.topShift, b.topShift),
|
||||
agentNames: a.agentNames,
|
||||
agentIDs: a.agentIDs,
|
||||
asynchronousY: nullableMax(a.asynchronousY, b.asynchronousY),
|
||||
};
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ define([
|
|||
renderPre(stage, env) {
|
||||
const baseResults = {
|
||||
topShift: 0,
|
||||
agentNames: [],
|
||||
agentIDs: [],
|
||||
asynchronousY: null,
|
||||
};
|
||||
|
||||
|
|
|
@ -171,7 +171,7 @@ define([
|
|||
top: 3,
|
||||
bottom: 2,
|
||||
},
|
||||
mode: {
|
||||
tag: {
|
||||
padding: {
|
||||
top: 1,
|
||||
left: 3,
|
||||
|
|
|
@ -180,7 +180,7 @@ define([
|
|||
top: 3,
|
||||
bottom: 4,
|
||||
},
|
||||
mode: {
|
||||
tag: {
|
||||
padding: {
|
||||
top: 2,
|
||||
left: 5,
|
||||
|
|
|
@ -178,7 +178,7 @@ define([
|
|||
top: 3,
|
||||
bottom: 2,
|
||||
},
|
||||
mode: {
|
||||
tag: {
|
||||
padding: {
|
||||
top: 2,
|
||||
left: 4,
|
||||
|
|
|
@ -164,7 +164,7 @@ define([
|
|||
top: 3,
|
||||
bottom: 2,
|
||||
},
|
||||
mode: {
|
||||
tag: {
|
||||
padding: {
|
||||
top: 2,
|
||||
left: 3,
|
||||
|
@ -376,8 +376,8 @@ define([
|
|||
|
||||
this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this);
|
||||
this.blocks[''].boxRenderer = this.renderBlock.bind(this);
|
||||
this.blocks.ref.section.mode.boxRenderer = this.renderTag;
|
||||
this.blocks[''].section.mode.boxRenderer = this.renderTag;
|
||||
this.blocks.ref.section.tag.boxRenderer = this.renderTag;
|
||||
this.blocks[''].section.tag.boxRenderer = this.renderTag;
|
||||
this.blocks[''].sepRenderer = this.renderSeparator.bind(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ define([
|
|||
'sequence/SequenceDiagram_spec',
|
||||
'sequence/Tokeniser_spec',
|
||||
'sequence/Parser_spec',
|
||||
'sequence/MarkdownParser_spec',
|
||||
'sequence/LabelPatternParser_spec',
|
||||
'sequence/Generator_spec',
|
||||
'sequence/Renderer_spec',
|
||||
|
|
|
@ -14,12 +14,14 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
}
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
constructor(container, initialState = {}) {
|
||||
this.container = container;
|
||||
this.state = {
|
||||
attrs: {},
|
||||
text: '',
|
||||
formatted: EMPTY,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
@ -57,18 +59,19 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
}
|
||||
|
||||
_renderText() {
|
||||
if(!this.state.text) {
|
||||
if(!this.state.formatted) {
|
||||
this._reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = this.state.text.split('\n');
|
||||
this._rebuildNodes(lines.length);
|
||||
const formatted = this.state.formatted;
|
||||
this._rebuildNodes(formatted.length);
|
||||
|
||||
let maxWidth = 0;
|
||||
this.nodes.forEach(({text, element}, i) => {
|
||||
text.nodeValue = lines[i];
|
||||
maxWidth = Math.max(maxWidth, lines[i].length);
|
||||
const ln = formatted[i].reduce((v, pt) => v + pt.text, '');
|
||||
text.nodeValue = ln;
|
||||
maxWidth = Math.max(maxWidth, ln.length);
|
||||
});
|
||||
this.width = maxWidth;
|
||||
}
|
||||
|
@ -100,12 +103,12 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
|
||||
if(this.state.attrs !== oldState.attrs) {
|
||||
this._reset();
|
||||
oldState.text = '';
|
||||
oldState.formatted = EMPTY;
|
||||
}
|
||||
|
||||
const oldNodes = this.nodes.length;
|
||||
|
||||
if(this.state.text !== oldState.text) {
|
||||
if(this.state.formatted !== oldState.formatted) {
|
||||
this._renderText();
|
||||
}
|
||||
|
||||
|
@ -120,29 +123,29 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
}
|
||||
|
||||
class SizeTester {
|
||||
measure(attrs, content) {
|
||||
if(!content) {
|
||||
measure(attrs, formatted) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
let width = 0;
|
||||
lines.forEach((line) => {
|
||||
width = Math.max(width, line.length);
|
||||
formatted.forEach((line) => {
|
||||
const length = line.reduce((v, pt) => v + pt.text.length, 0);
|
||||
width = Math.max(width, length);
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height: lines.length,
|
||||
height: formatted.length,
|
||||
};
|
||||
}
|
||||
|
||||
measureHeight(attrs, content) {
|
||||
if(!content) {
|
||||
measureHeight(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return content.split('\n').length;
|
||||
return formatted.length;
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
|
|
|
@ -46,24 +46,10 @@ define([
|
|||
return g;
|
||||
}
|
||||
|
||||
function renderBoxedText(text, {
|
||||
x,
|
||||
y,
|
||||
padding,
|
||||
boxAttrs,
|
||||
labelAttrs,
|
||||
boxLayer,
|
||||
labelLayer,
|
||||
boxRenderer = null,
|
||||
SVGTextBlockClass = SVGTextBlock,
|
||||
}) {
|
||||
if(!text) {
|
||||
return {width: 0, height: 0, label: null, box: null};
|
||||
}
|
||||
|
||||
function calculateAnchor(x, attrs, padding) {
|
||||
let shift = 0;
|
||||
let anchorX = x;
|
||||
switch(labelAttrs['text-anchor']) {
|
||||
switch(attrs['text-anchor']) {
|
||||
case 'middle':
|
||||
shift = 0.5;
|
||||
anchorX += (padding.left - padding.right) / 2;
|
||||
|
@ -77,10 +63,29 @@ define([
|
|||
anchorX += padding.left;
|
||||
break;
|
||||
}
|
||||
return {shift, anchorX};
|
||||
}
|
||||
|
||||
function renderBoxedText(formatted, {
|
||||
x,
|
||||
y,
|
||||
padding,
|
||||
boxAttrs,
|
||||
labelAttrs,
|
||||
boxLayer,
|
||||
labelLayer,
|
||||
boxRenderer = null,
|
||||
SVGTextBlockClass = SVGTextBlock,
|
||||
}) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0, label: null, box: null};
|
||||
}
|
||||
|
||||
const {shift, anchorX} = calculateAnchor(x, labelAttrs, padding);
|
||||
|
||||
const label = new SVGTextBlockClass(labelLayer, {
|
||||
attrs: labelAttrs,
|
||||
text,
|
||||
formatted,
|
||||
x: anchorX,
|
||||
y: y + padding.top,
|
||||
});
|
||||
|
|
|
@ -56,7 +56,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
describe('renderBoxedText', () => {
|
||||
it('renders a label', () => {
|
||||
const o = document.createElement('p');
|
||||
const rendered = SVGShapes.renderBoxedText('foo', {
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
|
||||
x: 1,
|
||||
y: 2,
|
||||
padding: {left: 4, top: 8, right: 16, bottom: 32},
|
||||
|
@ -65,7 +65,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
boxLayer: o,
|
||||
labelLayer: o,
|
||||
});
|
||||
expect(rendered.label.state.text).toEqual('foo');
|
||||
expect(rendered.label.state.formatted).toEqual([[{text: 'foo'}]]);
|
||||
expect(rendered.label.state.x).toEqual(5);
|
||||
expect(rendered.label.state.y).toEqual(10);
|
||||
expect(rendered.label.firstLine().parentNode).toEqual(o);
|
||||
|
@ -73,7 +73,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
|
||||
it('positions a box beneath the rendered label', () => {
|
||||
const o = document.createElement('p');
|
||||
const rendered = SVGShapes.renderBoxedText('foo', {
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
|
||||
x: 1,
|
||||
y: 2,
|
||||
padding: {left: 4, top: 8, right: 16, bottom: 32},
|
||||
|
@ -91,7 +91,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
|
||||
it('returns the size of the rendered box', () => {
|
||||
const o = document.createElement('p');
|
||||
const rendered = SVGShapes.renderBoxedText('foo', {
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
|
||||
x: 1,
|
||||
y: 2,
|
||||
padding: {left: 4, top: 8, right: 16, bottom: 32},
|
||||
|
|
|
@ -23,84 +23,107 @@ define(['./SVGUtilities'], (svg) => {
|
|||
}
|
||||
}
|
||||
|
||||
function populateSvgTextLine(node, formattedLine) {
|
||||
if(!Array.isArray(formattedLine)) {
|
||||
throw new Error('Invalid formatted text line: ' + formattedLine);
|
||||
}
|
||||
formattedLine.forEach(({text, attrs}) => {
|
||||
const textNode = svg.makeText(text);
|
||||
if(attrs) {
|
||||
const span = svg.make('tspan', attrs);
|
||||
span.appendChild(textNode);
|
||||
node.appendChild(span);
|
||||
} else {
|
||||
node.appendChild(textNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
constructor(container, initialState = {}) {
|
||||
this.container = container;
|
||||
this.state = {
|
||||
attrs: {},
|
||||
text: '',
|
||||
formatted: EMPTY,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.nodes = [];
|
||||
this.lines = [];
|
||||
this.set(initialState);
|
||||
}
|
||||
|
||||
_rebuildNodes(count) {
|
||||
if(count > this.nodes.length) {
|
||||
_rebuildLines(count) {
|
||||
if(count > this.lines.length) {
|
||||
const attrs = Object.assign({
|
||||
'x': this.state.x,
|
||||
}, this.state.attrs);
|
||||
|
||||
while(this.nodes.length < count) {
|
||||
const element = svg.make('text', attrs);
|
||||
const text = svg.makeText();
|
||||
element.appendChild(text);
|
||||
this.container.appendChild(element);
|
||||
this.nodes.push({element, text});
|
||||
while(this.lines.length < count) {
|
||||
const node = svg.make('text', attrs);
|
||||
this.container.appendChild(node);
|
||||
this.lines.push({node, latest: ''});
|
||||
}
|
||||
} else {
|
||||
while(this.nodes.length > count) {
|
||||
const {element} = this.nodes.pop();
|
||||
this.container.removeChild(element);
|
||||
while(this.lines.length > count) {
|
||||
const {node} = this.lines.pop();
|
||||
this.container.removeChild(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._rebuildNodes(0);
|
||||
this._rebuildLines(0);
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
_renderText() {
|
||||
if(!this.state.text) {
|
||||
const {formatted} = this.state;
|
||||
|
||||
if(!formatted || !formatted.length) {
|
||||
this._reset();
|
||||
return;
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
|
||||
const lines = this.state.text.split('\n');
|
||||
this._rebuildNodes(lines.length);
|
||||
this._rebuildLines(formatted.length);
|
||||
|
||||
let maxWidth = 0;
|
||||
this.nodes.forEach(({text, element}, i) => {
|
||||
if(text.nodeValue !== lines[i]) {
|
||||
text.nodeValue = lines[i];
|
||||
this.lines.forEach((ln, i) => {
|
||||
const id = JSON.stringify(formatted[i]);
|
||||
if(id !== ln.latest) {
|
||||
svg.empty(ln.node);
|
||||
populateSvgTextLine(ln.node, formatted[i]);
|
||||
ln.latest = id;
|
||||
}
|
||||
maxWidth = Math.max(maxWidth, element.getComputedTextLength());
|
||||
maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength());
|
||||
});
|
||||
this.width = maxWidth;
|
||||
}
|
||||
|
||||
_updateX() {
|
||||
this.nodes.forEach(({element}) => {
|
||||
element.setAttribute('x', this.state.x);
|
||||
this.lines.forEach(({node}) => {
|
||||
node.setAttribute('x', this.state.x);
|
||||
});
|
||||
}
|
||||
|
||||
_updateY() {
|
||||
const {size, lineHeight} = fontDetails(this.state.attrs);
|
||||
this.nodes.forEach(({element}, i) => {
|
||||
element.setAttribute('y', this.state.y + i * lineHeight + size);
|
||||
this.lines.forEach(({node}, i) => {
|
||||
node.setAttribute('y', this.state.y + i * lineHeight + size);
|
||||
});
|
||||
this.height = lineHeight * this.nodes.length;
|
||||
this.height = lineHeight * this.lines.length;
|
||||
}
|
||||
|
||||
firstLine() {
|
||||
if(this.nodes.length > 0) {
|
||||
return this.nodes[0].element;
|
||||
if(this.lines.length > 0) {
|
||||
return this.lines[0].node;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -112,12 +135,12 @@ define(['./SVGUtilities'], (svg) => {
|
|||
|
||||
if(this.state.attrs !== oldState.attrs) {
|
||||
this._reset();
|
||||
oldState.text = '';
|
||||
oldState.formatted = EMPTY;
|
||||
}
|
||||
|
||||
const oldNodes = this.nodes.length;
|
||||
const oldLines = this.lines.length;
|
||||
|
||||
if(this.state.text !== oldState.text) {
|
||||
if(this.state.formatted !== oldState.formatted) {
|
||||
this._renderText();
|
||||
}
|
||||
|
||||
|
@ -125,7 +148,7 @@ define(['./SVGUtilities'], (svg) => {
|
|||
this._updateX();
|
||||
}
|
||||
|
||||
if(this.state.y !== oldState.y || this.nodes.length !== oldNodes) {
|
||||
if(this.state.y !== oldState.y || this.lines.length !== oldLines) {
|
||||
this._updateY();
|
||||
}
|
||||
}
|
||||
|
@ -142,18 +165,18 @@ define(['./SVGUtilities'], (svg) => {
|
|||
this.cache = new Map();
|
||||
}
|
||||
|
||||
measure(attrs, content) {
|
||||
if(!content) {
|
||||
measure(attrs, formatted) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
|
||||
let tester = this.cache.get(attrs);
|
||||
if(!tester) {
|
||||
const text = svg.makeText();
|
||||
const node = svg.make('text', attrs);
|
||||
node.appendChild(text);
|
||||
this.testers.appendChild(node);
|
||||
tester = {text, node};
|
||||
tester = svg.make('text', attrs);
|
||||
this.testers.appendChild(tester);
|
||||
this.cache.set(attrs, tester);
|
||||
}
|
||||
|
||||
|
@ -161,26 +184,28 @@ define(['./SVGUtilities'], (svg) => {
|
|||
this.container.appendChild(this.testers);
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
let width = 0;
|
||||
lines.forEach((line) => {
|
||||
tester.text.nodeValue = line;
|
||||
width = Math.max(width, tester.node.getComputedTextLength());
|
||||
formatted.forEach((line) => {
|
||||
svg.empty(tester);
|
||||
populateSvgTextLine(tester, line);
|
||||
width = Math.max(width, tester.getComputedTextLength());
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height: lines.length * fontDetails(attrs).lineHeight,
|
||||
height: formatted.length * fontDetails(attrs).lineHeight,
|
||||
};
|
||||
}
|
||||
|
||||
measureHeight(attrs, content) {
|
||||
if(!content) {
|
||||
measureHeight(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
return 0;
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
return lines.length * fontDetails(attrs).lineHeight;
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
|
|
|
@ -23,7 +23,7 @@ defineDescribe('SVGTextBlock', [
|
|||
|
||||
describe('constructor', () => {
|
||||
it('defaults to blank text at 0, 0', () => {
|
||||
expect(block.state.text).toEqual('');
|
||||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.state.x).toEqual(0);
|
||||
expect(block.state.y).toEqual(0);
|
||||
expect(hold.children.length).toEqual(0);
|
||||
|
@ -31,15 +31,18 @@ defineDescribe('SVGTextBlock', [
|
|||
|
||||
it('does not explode if given no setup', () => {
|
||||
block = new SVGTextBlock(hold);
|
||||
expect(block.state.text).toEqual('');
|
||||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.state.x).toEqual(0);
|
||||
expect(block.state.y).toEqual(0);
|
||||
expect(hold.children.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('adds the given text if specified', () => {
|
||||
block = new SVGTextBlock(hold, {attrs, text: 'abc'});
|
||||
expect(block.state.text).toEqual('abc');
|
||||
it('adds the given formatted text if specified', () => {
|
||||
block = new SVGTextBlock(hold, {
|
||||
attrs,
|
||||
formatted: [[{text: 'abc'}]],
|
||||
});
|
||||
expect(block.state.formatted).toEqual([[{text: 'abc'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
});
|
||||
|
||||
|
@ -52,31 +55,35 @@ defineDescribe('SVGTextBlock', [
|
|||
|
||||
describe('.set', () => {
|
||||
it('sets the text to the given content', () => {
|
||||
block.set({text: 'foo'});
|
||||
expect(block.state.text).toEqual('foo');
|
||||
block.set({formatted: [[{text: 'foo'}]]});
|
||||
expect(block.state.formatted).toEqual([[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.children[0].innerHTML).toEqual('foo');
|
||||
});
|
||||
|
||||
it('renders multiline text', () => {
|
||||
block.set({text: 'foo\nbar'});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
expect(hold.children.length).toEqual(2);
|
||||
expect(hold.children[0].innerHTML).toEqual('foo');
|
||||
expect(hold.children[1].innerHTML).toEqual('bar');
|
||||
});
|
||||
|
||||
it('populates width and height with the size of the text', () => {
|
||||
block.set({text: 'foo\nbar'});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
expect(block.width).toBeGreaterThan(0);
|
||||
expect(block.height).toEqual(30);
|
||||
});
|
||||
|
||||
it('re-uses text nodes when possible, adding more if needed', () => {
|
||||
block.set({text: 'foo\nbar'});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
const line0 = hold.children[0];
|
||||
const line1 = hold.children[1];
|
||||
|
||||
block.set({text: 'zig\nzag\nbaz'});
|
||||
block.set({formatted: [
|
||||
[{text: 'zig'}],
|
||||
[{text: 'zag'}],
|
||||
[{text: 'baz'}],
|
||||
]});
|
||||
|
||||
expect(hold.children.length).toEqual(3);
|
||||
expect(hold.children[0]).toEqual(line0);
|
||||
|
@ -87,10 +94,10 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
it('re-uses text nodes when possible, removing extra if needed', () => {
|
||||
block.set({text: 'foo\nbar'});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
const line0 = hold.children[0];
|
||||
|
||||
block.set({text: 'zig'});
|
||||
block.set({formatted: [[{text: 'zig'}]]});
|
||||
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.children[0]).toEqual(line0);
|
||||
|
@ -98,7 +105,7 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
it('positions text nodes and applies attributes', () => {
|
||||
block.set({text: 'foo\nbar'});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
expect(hold.children.length).toEqual(2);
|
||||
expect(hold.children[0].getAttribute('x')).toEqual('0');
|
||||
expect(hold.children[0].getAttribute('y')).toEqual('10');
|
||||
|
@ -109,7 +116,7 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
it('moves all nodes', () => {
|
||||
block.set({text: 'foo\nbaz'});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
block.set({x: 5, y: 7});
|
||||
expect(hold.children[0].getAttribute('x')).toEqual('5');
|
||||
expect(hold.children[0].getAttribute('y')).toEqual('17');
|
||||
|
@ -118,10 +125,10 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
it('clears if the text is empty', () => {
|
||||
block.set({text: 'foo\nbaz'});
|
||||
block.set({text: ''});
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
block.set({formatted: []});
|
||||
expect(hold.children.length).toEqual(0);
|
||||
expect(block.state.text).toEqual('');
|
||||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.width).toEqual(0);
|
||||
expect(block.height).toEqual(0);
|
||||
});
|
||||
|
@ -135,28 +142,39 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
describe('.measure', () => {
|
||||
it('calculates the size of the rendered text', () => {
|
||||
const size = tester.measure(attrs, 'foo');
|
||||
it('calculates the size of the formatted text', () => {
|
||||
const size = tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toEqual(15);
|
||||
});
|
||||
|
||||
it('measures multiline text', () => {
|
||||
const size = tester.measure(attrs, 'foo\nbar');
|
||||
const size = tester.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
[{text: 'bar'}],
|
||||
]);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toEqual(30);
|
||||
});
|
||||
|
||||
it('returns 0, 0 for empty content', () => {
|
||||
const size = tester.measure(attrs, '');
|
||||
const size = tester.measure(attrs, []);
|
||||
expect(size.width).toEqual(0);
|
||||
expect(size.height).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns the maximum width for multiline text', () => {
|
||||
const size0 = tester.measure(attrs, 'foo');
|
||||
const size1 = tester.measure(attrs, 'longline');
|
||||
const size = tester.measure(attrs, 'foo\nlongline\nfoo');
|
||||
const size0 = tester.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
]);
|
||||
const size1 = tester.measure(attrs, [
|
||||
[{text: 'longline'}],
|
||||
]);
|
||||
const size = tester.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
[{text: 'longline'}],
|
||||
[{text: 'foo'}],
|
||||
]);
|
||||
expect(size1.width).toBeGreaterThan(size0.width);
|
||||
expect(size.width).toEqual(size1.width);
|
||||
});
|
||||
|
@ -164,39 +182,44 @@ defineDescribe('SVGTextBlock', [
|
|||
|
||||
describe('.measureHeight', () => {
|
||||
it('calculates the height of the rendered text', () => {
|
||||
const height = tester.measureHeight(attrs, 'foo');
|
||||
const height = tester.measureHeight(attrs, [
|
||||
[{text: 'foo'}],
|
||||
]);
|
||||
expect(height).toEqual(15);
|
||||
});
|
||||
|
||||
it('measures multiline text', () => {
|
||||
const height = tester.measureHeight(attrs, 'foo\nbar');
|
||||
const height = tester.measureHeight(attrs, [
|
||||
[{text: 'foo'}],
|
||||
[{text: 'bar'}],
|
||||
]);
|
||||
expect(height).toEqual(30);
|
||||
});
|
||||
|
||||
it('returns 0 for empty content', () => {
|
||||
const height = tester.measureHeight(attrs, '');
|
||||
const height = tester.measureHeight(attrs, []);
|
||||
expect(height).toEqual(0);
|
||||
});
|
||||
|
||||
it('does not require the container', () => {
|
||||
tester.measureHeight(attrs, 'foo');
|
||||
tester.measureHeight(attrs, [[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.detach', () => {
|
||||
it('removes the test node from the DOM', () => {
|
||||
tester.measure(attrs, 'foo');
|
||||
tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
tester.detach();
|
||||
expect(hold.children.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('does not prevent using the tester again later', () => {
|
||||
tester.measure(attrs, 'foo');
|
||||
tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
tester.detach();
|
||||
|
||||
const size = tester.measure(attrs, 'foo');
|
||||
const size = tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue