687 lines
18 KiB
JavaScript
687 lines
18 KiB
JavaScript
/* jshint -W072 */ // Allow several required modules
|
|
define([
|
|
'core/ArrayUtilities',
|
|
'core/EventObject',
|
|
'svg/SVGUtilities',
|
|
'svg/SVGShapes',
|
|
'./components/BaseComponent',
|
|
'./components/Marker',
|
|
'./components/AgentCap',
|
|
'./components/AgentHighlight',
|
|
'./components/Connect',
|
|
'./components/Note',
|
|
], (
|
|
array,
|
|
EventObject,
|
|
svg,
|
|
SVGShapes,
|
|
BaseComponent
|
|
) => {
|
|
/* jshint +W072 */
|
|
'use strict';
|
|
|
|
function traverse(stages, callbacks) {
|
|
stages.forEach((stage) => {
|
|
if(stage.type === 'block') {
|
|
const scope = {};
|
|
if(callbacks.blockBeginFn) {
|
|
callbacks.blockBeginFn(scope, stage);
|
|
}
|
|
stage.sections.forEach((section) => {
|
|
if(callbacks.sectionBeginFn) {
|
|
callbacks.sectionBeginFn(scope, stage, section);
|
|
}
|
|
traverse(section.stages, callbacks);
|
|
if(callbacks.sectionEndFn) {
|
|
callbacks.sectionEndFn(scope, stage, section);
|
|
}
|
|
});
|
|
if(callbacks.blockEndFn) {
|
|
callbacks.blockEndFn(scope, stage);
|
|
}
|
|
} else if(callbacks.stagesFn) {
|
|
if(stage.type === 'parallel') {
|
|
callbacks.stagesFn(stage.stages);
|
|
} else {
|
|
callbacks.stagesFn([stage]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function findExtremes(agentInfos, agentNames) {
|
|
let min = null;
|
|
let max = null;
|
|
agentNames.forEach((name) => {
|
|
const info = agentInfos.get(name);
|
|
if(min === null || info.index < min.index) {
|
|
min = info;
|
|
}
|
|
if(max === null || info.index > max.index) {
|
|
max = info;
|
|
}
|
|
});
|
|
return {
|
|
left: min.label,
|
|
right: max.label,
|
|
};
|
|
}
|
|
|
|
function makeThemes(themes) {
|
|
if(themes.length === 0) {
|
|
throw new Error('Cannot render without a theme');
|
|
}
|
|
const themeMap = new Map();
|
|
themes.forEach((theme) => {
|
|
themeMap.set(theme.name, theme);
|
|
});
|
|
themeMap.set('', themes[0]);
|
|
return themeMap;
|
|
}
|
|
|
|
let globalNamespace = 0;
|
|
|
|
function parseNamespace(namespace) {
|
|
if(namespace === null) {
|
|
namespace = 'R' + globalNamespace;
|
|
++ globalNamespace;
|
|
}
|
|
return namespace;
|
|
}
|
|
|
|
return class Renderer extends EventObject {
|
|
constructor({
|
|
themes = [],
|
|
namespace = null,
|
|
components = null,
|
|
SVGTextBlockClass = SVGShapes.TextBlock,
|
|
} = {}) {
|
|
super();
|
|
|
|
if(components === null) {
|
|
components = BaseComponent.getComponents();
|
|
}
|
|
|
|
this.separationTraversalFns = {
|
|
stagesFn: this.separationStages.bind(this),
|
|
blockBeginFn: this.separationBlockBegin.bind(this),
|
|
sectionBeginFn: this.separationSectionBegin.bind(this),
|
|
blockEndFn: this.separationBlockEnd.bind(this),
|
|
};
|
|
|
|
this.renderTraversalFns = {
|
|
stagesFn: this.renderStages.bind(this),
|
|
blockBeginFn: this.renderBlockBegin.bind(this),
|
|
sectionBeginFn: this.renderSectionBegin.bind(this),
|
|
sectionEndFn: this.renderSectionEnd.bind(this),
|
|
blockEndFn: this.renderBlockEnd.bind(this),
|
|
};
|
|
|
|
this.addSeparation = this.addSeparation.bind(this);
|
|
this.addDef = this.addDef.bind(this);
|
|
|
|
this.state = {};
|
|
this.width = 0;
|
|
this.height = 0;
|
|
this.themes = makeThemes(themes);
|
|
this.theme = null;
|
|
this.namespace = parseNamespace(namespace);
|
|
this.components = components;
|
|
this.SVGTextBlockClass = SVGTextBlockClass;
|
|
this.knownDefs = new Set();
|
|
this.highlights = new Map();
|
|
this.currentHighlight = -1;
|
|
this.buildStaticElements();
|
|
this.components.forEach((component) => {
|
|
component.makeState(this.state);
|
|
});
|
|
}
|
|
|
|
clone({namespace = null} = {}) {
|
|
return new Renderer({
|
|
themes: this.getThemes(),
|
|
namespace,
|
|
components: this.components,
|
|
SVGTextBlockClass: this.SVGTextBlockClass,
|
|
});
|
|
}
|
|
|
|
buildStaticElements() {
|
|
this.base = svg.makeContainer();
|
|
|
|
this.defs = svg.make('defs');
|
|
this.mask = svg.make('mask', {
|
|
'id': this.namespace + 'LineMask',
|
|
'maskUnits': 'userSpaceOnUse',
|
|
});
|
|
this.maskReveal = svg.make('rect', {'fill': '#FFFFFF'});
|
|
this.agentLines = svg.make('g', {
|
|
'mask': 'url(#' + this.namespace + 'LineMask)',
|
|
});
|
|
this.blocks = svg.make('g');
|
|
this.sections = svg.make('g');
|
|
this.actionShapes = svg.make('g');
|
|
this.actionLabels = svg.make('g');
|
|
this.base.appendChild(this.defs);
|
|
this.base.appendChild(this.agentLines);
|
|
this.base.appendChild(this.blocks);
|
|
this.base.appendChild(this.sections);
|
|
this.base.appendChild(this.actionShapes);
|
|
this.base.appendChild(this.actionLabels);
|
|
this.title = new this.SVGTextBlockClass(this.base);
|
|
|
|
this.sizer = new this.SVGTextBlockClass.SizeTester(this.base);
|
|
}
|
|
|
|
addDef(name, generator) {
|
|
const namespacedName = this.namespace + name;
|
|
if(this.knownDefs.has(name)) {
|
|
return namespacedName;
|
|
}
|
|
this.knownDefs.add(name);
|
|
const def = generator();
|
|
def.setAttribute('id', namespacedName);
|
|
this.defs.appendChild(def);
|
|
return namespacedName;
|
|
}
|
|
|
|
addSeparation(agentName1, agentName2, dist) {
|
|
const info1 = this.agentInfos.get(agentName1);
|
|
const info2 = this.agentInfos.get(agentName2);
|
|
|
|
const d1 = info1.separations.get(agentName2) || 0;
|
|
info1.separations.set(agentName2, Math.max(d1, dist));
|
|
|
|
const d2 = info2.separations.get(agentName1) || 0;
|
|
info2.separations.set(agentName1, Math.max(d2, dist));
|
|
}
|
|
|
|
separationBlockBegin(scope, {left, right}) {
|
|
array.mergeSets(this.visibleAgents, [left, right]);
|
|
}
|
|
|
|
separationSectionBegin(scope, {left, right}, {mode, label}) {
|
|
const config = this.theme.block.section;
|
|
const width = (
|
|
this.sizer.measure(config.mode.labelAttrs, mode).width +
|
|
config.mode.padding.left +
|
|
config.mode.padding.right +
|
|
this.sizer.measure(config.label.labelAttrs, label).width +
|
|
config.label.padding.left +
|
|
config.label.padding.right
|
|
);
|
|
this.addSeparation(left, right, width);
|
|
}
|
|
|
|
separationBlockEnd(scope, {left, right}) {
|
|
array.removeAll(this.visibleAgents, [left, right]);
|
|
}
|
|
|
|
separationStages(stages) {
|
|
const agentSpaces = new Map();
|
|
const agentNames = this.visibleAgents.slice();
|
|
|
|
const addSpacing = (agentName, {left, right}) => {
|
|
const current = agentSpaces.get(agentName);
|
|
current.left = Math.max(current.left, left);
|
|
current.right = Math.max(current.right, right);
|
|
};
|
|
|
|
this.agentInfos.forEach((agentInfo) => {
|
|
const rad = agentInfo.currentRad;
|
|
agentInfo.currentMaxRad = rad;
|
|
agentSpaces.set(agentInfo.label, {left: rad, right: rad});
|
|
});
|
|
const env = {
|
|
theme: this.theme,
|
|
agentInfos: this.agentInfos,
|
|
visibleAgents: this.visibleAgents,
|
|
textSizer: this.sizer,
|
|
addSpacing,
|
|
addSeparation: this.addSeparation,
|
|
};
|
|
stages.forEach((stage) => {
|
|
this.components.get(stage.type).separationPre(stage, env);
|
|
});
|
|
stages.forEach((stage) => {
|
|
this.components.get(stage.type).separation(stage, env);
|
|
});
|
|
array.mergeSets(agentNames, this.visibleAgents);
|
|
|
|
agentNames.forEach((agentNameR) => {
|
|
const infoR = this.agentInfos.get(agentNameR);
|
|
const sepR = agentSpaces.get(agentNameR);
|
|
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);
|
|
if(infoL.index >= infoR.index) {
|
|
return;
|
|
}
|
|
const sepL = agentSpaces.get(agentNameL);
|
|
this.addSeparation(
|
|
agentNameR,
|
|
agentNameL,
|
|
sepR.left + sepL.right + this.theme.agentMargin
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
checkAgentRange(agentNames, topY = 0) {
|
|
if(agentNames.length === 0) {
|
|
return topY;
|
|
}
|
|
const {left, right} = findExtremes(this.agentInfos, agentNames);
|
|
const leftX = this.agentInfos.get(left).x;
|
|
const rightX = this.agentInfos.get(right).x;
|
|
let baseY = topY;
|
|
this.agentInfos.forEach((agentInfo) => {
|
|
if(agentInfo.x >= leftX && agentInfo.x <= rightX) {
|
|
baseY = Math.max(baseY, agentInfo.latestY);
|
|
}
|
|
});
|
|
return baseY;
|
|
}
|
|
|
|
markAgentRange(agentNames, y) {
|
|
if(agentNames.length === 0) {
|
|
return;
|
|
}
|
|
const {left, right} = findExtremes(this.agentInfos, agentNames);
|
|
const leftX = this.agentInfos.get(left).x;
|
|
const rightX = this.agentInfos.get(right).x;
|
|
this.agentInfos.forEach((agentInfo) => {
|
|
if(agentInfo.x >= leftX && agentInfo.x <= rightX) {
|
|
agentInfo.latestY = y;
|
|
}
|
|
});
|
|
}
|
|
|
|
drawAgentLine(agentInfo, toY) {
|
|
if(
|
|
agentInfo.latestYStart === null ||
|
|
toY <= agentInfo.latestYStart
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const r = agentInfo.currentRad;
|
|
|
|
if(r > 0) {
|
|
this.agentLines.appendChild(svg.make('rect', Object.assign({
|
|
'x': agentInfo.x - r,
|
|
'y': agentInfo.latestYStart,
|
|
'width': r * 2,
|
|
'height': toY - agentInfo.latestYStart,
|
|
'class': 'agent-' + agentInfo.index + '-line',
|
|
}, this.theme.agentLineAttrs)));
|
|
} else {
|
|
this.agentLines.appendChild(svg.make('line', Object.assign({
|
|
'x1': agentInfo.x,
|
|
'y1': agentInfo.latestYStart,
|
|
'x2': agentInfo.x,
|
|
'y2': toY,
|
|
'class': 'agent-' + agentInfo.index + '-line',
|
|
}, this.theme.agentLineAttrs)));
|
|
}
|
|
}
|
|
|
|
renderBlockBegin(scope, {left, right}) {
|
|
this.currentY = (
|
|
this.checkAgentRange([left, right], this.currentY) +
|
|
this.theme.block.margin.top
|
|
);
|
|
|
|
scope.y = this.currentY;
|
|
scope.first = true;
|
|
this.markAgentRange([left, right], this.currentY);
|
|
}
|
|
|
|
renderSectionBegin(scope, {left, right}, {mode, label}) {
|
|
this.currentY = this.checkAgentRange([left, right], this.currentY);
|
|
const config = this.theme.block;
|
|
const agentInfoL = this.agentInfos.get(left);
|
|
const agentInfoR = this.agentInfos.get(right);
|
|
|
|
if(scope.first) {
|
|
scope.first = false;
|
|
} else {
|
|
this.currentY += config.section.padding.bottom;
|
|
this.sections.appendChild(svg.make('line', Object.assign({
|
|
'x1': agentInfoL.x,
|
|
'y1': this.currentY,
|
|
'x2': agentInfoR.x,
|
|
'y2': this.currentY,
|
|
}, config.separator.attrs)));
|
|
}
|
|
|
|
const modeRender = SVGShapes.renderBoxedText(mode, {
|
|
x: agentInfoL.x,
|
|
y: this.currentY,
|
|
padding: config.section.mode.padding,
|
|
boxAttrs: config.section.mode.boxAttrs,
|
|
labelAttrs: config.section.mode.labelAttrs,
|
|
boxLayer: this.blocks,
|
|
labelLayer: this.actionLabels,
|
|
SVGTextBlockClass: this.SVGTextBlockClass,
|
|
});
|
|
|
|
const labelRender = SVGShapes.renderBoxedText(label, {
|
|
x: agentInfoL.x + modeRender.width,
|
|
y: this.currentY,
|
|
padding: config.section.label.padding,
|
|
boxAttrs: {'fill': '#000000'},
|
|
labelAttrs: config.section.label.labelAttrs,
|
|
boxLayer: this.mask,
|
|
labelLayer: this.actionLabels,
|
|
SVGTextBlockClass: this.SVGTextBlockClass,
|
|
});
|
|
|
|
this.currentY += (
|
|
Math.max(modeRender.height, labelRender.height) +
|
|
config.section.padding.top
|
|
);
|
|
this.markAgentRange([left, right], this.currentY);
|
|
}
|
|
|
|
renderSectionEnd(/*scope, block, section*/) {
|
|
}
|
|
|
|
renderBlockEnd(scope, {left, right}) {
|
|
const config = this.theme.block;
|
|
this.currentY = (
|
|
this.checkAgentRange([left, right], this.currentY) +
|
|
config.section.padding.bottom
|
|
);
|
|
|
|
const agentInfoL = this.agentInfos.get(left);
|
|
const agentInfoR = this.agentInfos.get(right);
|
|
this.blocks.appendChild(svg.make('rect', Object.assign({
|
|
'x': agentInfoL.x,
|
|
'y': scope.y,
|
|
'width': agentInfoR.x - agentInfoL.x,
|
|
'height': this.currentY - scope.y,
|
|
}, config.boxAttrs)));
|
|
|
|
this.currentY += config.margin.bottom + this.theme.actionMargin;
|
|
this.markAgentRange([left, right], this.currentY);
|
|
}
|
|
|
|
addHighlightObject(line, o) {
|
|
let list = this.highlights.get(line);
|
|
if(!list) {
|
|
list = [];
|
|
this.highlights.set(line, list);
|
|
}
|
|
list.push(o);
|
|
}
|
|
|
|
renderStages(stages) {
|
|
this.agentInfos.forEach((agentInfo) => {
|
|
const rad = agentInfo.currentRad;
|
|
agentInfo.currentMaxRad = rad;
|
|
});
|
|
|
|
let topY = 0;
|
|
let maxTopShift = 0;
|
|
let sequential = true;
|
|
const envPre = {
|
|
theme: this.theme,
|
|
agentInfos: this.agentInfos,
|
|
textSizer: this.sizer,
|
|
state: this.state,
|
|
};
|
|
const touchedAgentNames = [];
|
|
stages.forEach((stage) => {
|
|
const component = this.components.get(stage.type);
|
|
const r = component.renderPre(stage, envPre) || {};
|
|
if(r.topShift !== undefined) {
|
|
maxTopShift = Math.max(maxTopShift, r.topShift);
|
|
}
|
|
if(r.agentNames) {
|
|
array.mergeSets(touchedAgentNames, r.agentNames);
|
|
}
|
|
if(r.asynchronousY !== undefined) {
|
|
topY = Math.max(topY, r.asynchronousY);
|
|
sequential = false;
|
|
}
|
|
});
|
|
topY = this.checkAgentRange(touchedAgentNames, topY);
|
|
if(sequential) {
|
|
topY = Math.max(topY, this.currentY);
|
|
}
|
|
|
|
const env = {
|
|
topY,
|
|
primaryY: topY + maxTopShift,
|
|
shapeLayer: this.actionShapes,
|
|
labelLayer: this.actionLabels,
|
|
maskLayer: this.mask,
|
|
theme: this.theme,
|
|
agentInfos: this.agentInfos,
|
|
textSizer: this.sizer,
|
|
SVGTextBlockClass: this.SVGTextBlockClass,
|
|
state: this.state,
|
|
drawAgentLine: (agentName, toY, andStop = false) => {
|
|
const agentInfo = this.agentInfos.get(agentName);
|
|
this.drawAgentLine(agentInfo, toY);
|
|
agentInfo.latestYStart = andStop ? null : toY;
|
|
},
|
|
addDef: this.addDef,
|
|
};
|
|
let bottomY = topY;
|
|
stages.forEach((stage) => {
|
|
const eventOver = () => {
|
|
this.trigger('mouseover', [stage]);
|
|
};
|
|
|
|
const eventOut = () => {
|
|
this.trigger('mouseout');
|
|
};
|
|
|
|
const eventClick = () => {
|
|
this.trigger('click', [stage]);
|
|
};
|
|
|
|
env.makeRegion = (o) => {
|
|
if(!o) {
|
|
o = svg.make('g');
|
|
}
|
|
this.addHighlightObject(stage.ln, o);
|
|
o.setAttribute('class', 'region');
|
|
o.addEventListener('mouseenter', eventOver);
|
|
o.addEventListener('mouseleave', eventOut);
|
|
o.addEventListener('click', eventClick);
|
|
this.actionLabels.appendChild(o);
|
|
return o;
|
|
};
|
|
|
|
const component = this.components.get(stage.type);
|
|
const baseY = component.render(stage, env);
|
|
if(baseY !== undefined) {
|
|
bottomY = Math.max(bottomY, baseY);
|
|
}
|
|
});
|
|
this.markAgentRange(touchedAgentNames, bottomY);
|
|
|
|
this.currentY = bottomY;
|
|
}
|
|
|
|
positionAgents() {
|
|
// Map guarantees insertion-order iteration
|
|
const orderedInfos = [];
|
|
this.agentInfos.forEach((agentInfo) => {
|
|
let currentX = 0;
|
|
agentInfo.separations.forEach((dist, otherAgent) => {
|
|
const otherAgentInfo = this.agentInfos.get(otherAgent);
|
|
if(otherAgentInfo.index < agentInfo.index) {
|
|
currentX = Math.max(currentX, otherAgentInfo.x + dist);
|
|
}
|
|
});
|
|
agentInfo.x = currentX;
|
|
orderedInfos.push(agentInfo);
|
|
});
|
|
|
|
let previousInfo = {x: 0};
|
|
orderedInfos.reverse().forEach((agentInfo) => {
|
|
let currentX = previousInfo.x;
|
|
previousInfo = agentInfo;
|
|
if(!agentInfo.anchorRight) {
|
|
return;
|
|
}
|
|
agentInfo.separations.forEach((dist, otherAgent) => {
|
|
const otherAgentInfo = this.agentInfos.get(otherAgent);
|
|
if(otherAgentInfo.index > agentInfo.index) {
|
|
currentX = Math.min(currentX, otherAgentInfo.x - dist);
|
|
}
|
|
});
|
|
agentInfo.x = currentX;
|
|
});
|
|
|
|
this.agentInfos.forEach(({label, x, maxRPad, maxLPad}) => {
|
|
this.minX = Math.min(this.minX, x - maxLPad);
|
|
this.maxX = Math.max(this.maxX, x + maxRPad);
|
|
});
|
|
}
|
|
|
|
buildAgentInfos(agents, stages) {
|
|
this.agentInfos = new Map();
|
|
agents.forEach((agent, index) => {
|
|
this.agentInfos.set(agent.name, {
|
|
label: agent.name,
|
|
anchorRight: agent.anchorRight,
|
|
index,
|
|
x: null,
|
|
latestYStart: null,
|
|
currentRad: 0,
|
|
currentMaxRad: 0,
|
|
latestY: 0,
|
|
maxRPad: 0,
|
|
maxLPad: 0,
|
|
separations: new Map(),
|
|
});
|
|
});
|
|
|
|
this.visibleAgents = ['[', ']'];
|
|
traverse(stages, this.separationTraversalFns);
|
|
|
|
this.positionAgents();
|
|
}
|
|
|
|
updateBounds(stagesHeight) {
|
|
const cx = (this.minX + this.maxX) / 2;
|
|
const titleY = ((this.title.height > 0) ?
|
|
(-this.theme.titleMargin - this.title.height) : 0
|
|
);
|
|
this.title.set({x: cx, y: titleY});
|
|
|
|
const halfTitleWidth = this.title.width / 2;
|
|
const margin = this.theme.outerMargin;
|
|
const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
|
|
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
|
|
const y0 = titleY - margin;
|
|
const y1 = stagesHeight + margin;
|
|
|
|
this.maskReveal.setAttribute('x', x0);
|
|
this.maskReveal.setAttribute('y', y0);
|
|
this.maskReveal.setAttribute('width', x1 - x0);
|
|
this.maskReveal.setAttribute('height', y1 - y0);
|
|
|
|
this.base.setAttribute('viewBox', (
|
|
x0 + ' ' + y0 + ' ' +
|
|
(x1 - x0) + ' ' + (y1 - y0)
|
|
));
|
|
this.width = (x1 - x0);
|
|
this.height = (y1 - y0);
|
|
}
|
|
|
|
_reset() {
|
|
this.knownDefs.clear();
|
|
this.highlights.clear();
|
|
this.currentHighlight = -1;
|
|
svg.empty(this.defs);
|
|
svg.empty(this.mask);
|
|
svg.empty(this.agentLines);
|
|
svg.empty(this.blocks);
|
|
svg.empty(this.sections);
|
|
svg.empty(this.actionShapes);
|
|
svg.empty(this.actionLabels);
|
|
this.mask.appendChild(this.maskReveal);
|
|
this.defs.appendChild(this.mask);
|
|
this.components.forEach((component) => {
|
|
component.resetState(this.state);
|
|
});
|
|
}
|
|
|
|
setHighlight(line = null) {
|
|
if(line === null || !this.highlights.has(line)) {
|
|
line = -1;
|
|
}
|
|
if(this.currentHighlight === line) {
|
|
return;
|
|
}
|
|
if(this.currentHighlight !== -1) {
|
|
this.highlights.get(this.currentHighlight).forEach((o) => {
|
|
o.setAttribute('class', 'region');
|
|
});
|
|
}
|
|
if(line !== -1) {
|
|
this.highlights.get(line).forEach((o) => {
|
|
o.setAttribute('class', 'region focus');
|
|
});
|
|
}
|
|
this.currentHighlight = line;
|
|
}
|
|
|
|
render(sequence) {
|
|
const prevHighlight = this.currentHighlight;
|
|
this._reset();
|
|
|
|
const themeName = sequence.meta.theme;
|
|
this.theme = this.themes.get(themeName);
|
|
if(!this.theme) {
|
|
this.theme = this.themes.get('');
|
|
}
|
|
|
|
this.title.set({
|
|
attrs: this.theme.titleAttrs,
|
|
text: sequence.meta.title,
|
|
});
|
|
|
|
this.minX = 0;
|
|
this.maxX = 0;
|
|
this.buildAgentInfos(sequence.agents, sequence.stages);
|
|
|
|
this.currentY = 0;
|
|
traverse(sequence.stages, this.renderTraversalFns);
|
|
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
|
|
|
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
|
this.updateBounds(stagesHeight);
|
|
|
|
this.sizer.resetCache();
|
|
this.sizer.detach();
|
|
this.setHighlight(prevHighlight);
|
|
}
|
|
|
|
getThemeNames() {
|
|
return (Array.from(this.themes.keys())
|
|
.filter((name) => (name !== ''))
|
|
);
|
|
}
|
|
|
|
getThemes() {
|
|
return this.getThemeNames().map((name) => this.themes.get(name));
|
|
}
|
|
|
|
getAgentX(name) {
|
|
return this.agentInfos.get(name).x;
|
|
}
|
|
|
|
svg() {
|
|
return this.base;
|
|
}
|
|
};
|
|
});
|