Support rendering conditional boxes [#1]

This commit is contained in:
David Evans 2017-10-25 00:04:53 +01:00
parent 5ecce9a6ab
commit 1929e3ffb1
4 changed files with 347 additions and 108 deletions

View File

@ -36,6 +36,7 @@ define(() => {
return class Generator {
constructor() {
this.agentStates = new Map();
this.agents = [];
this.blockCount = 0;
this.nesting = [];
this.currentSection = null;
@ -52,18 +53,13 @@ define(() => {
this.handleStage = this.handleStage.bind(this);
}
addStage(stage) {
this.currentSection.stages.push(stage);
mergeSets(this.currentNest.agents, stage.agents);
}
addColumnBounds(target, agentL, agentR, involvedAgents = []) {
addBounds(target, agentL, agentR, involvedAgents = null) {
removeElement(target, agentL);
removeElement(target, agentR);
let indexL = 0;
let indexR = target.length;
if(involvedAgents.length > 0) {
if(involvedAgents) {
const found = (involvedAgents
.map((agent) => target.indexOf(agent))
.filter((p) => (p !== -1))
@ -103,18 +99,21 @@ define(() => {
const existing = lastElement(this.currentSection.stages) || {};
if(existing.type === type && existing.mode === mode) {
mergeSets(existing.agents, filteredAgents);
mergeSets(this.currentNest.agents, filteredAgents);
} else {
this.addStage({
this.currentSection.stages.push({
type,
agents: filteredAgents,
mode,
});
}
mergeSets(this.currentNest.agents, filteredAgents);
mergeSets(this.agents, filteredAgents);
}
beginNested(mode, label, name) {
const agents = [];
const nameL = name + '[';
const nameR = name + ']';
const agents = [nameL, nameR];
const stages = [];
this.currentSection = {
mode,
@ -122,20 +121,24 @@ define(() => {
stages,
};
this.currentNest = {
type: 'block',
agents,
sections: [this.currentSection],
leftColumn: name + '[',
rightColumn: name + ']',
stage: {
type: 'block',
sections: [this.currentSection],
left: nameL,
right: nameR,
},
};
this.agentStates.set(name + '[', LOCKED_AGENT);
this.agentStates.set(name + ']', LOCKED_AGENT);
this.agentStates.set(nameL, LOCKED_AGENT);
this.agentStates.set(nameR, LOCKED_AGENT);
this.nesting.push(this.currentNest);
return {agents, stages};
}
handleAgentDefine() {
handleAgentDefine({agents}) {
mergeSets(this.currentNest.agents, agents);
mergeSets(this.agents, agents);
}
handleAgentBegin({agents, mode}) {
@ -153,7 +156,7 @@ define(() => {
}
handleBlockSplit({mode, label}) {
if(this.currentNest.sections[0].mode !== 'if') {
if(this.currentNest.stage.sections[0].mode !== 'if') {
throw new Error('Invalid block nesting');
}
this.currentSection = {
@ -161,35 +164,34 @@ define(() => {
label,
stages: [],
};
this.currentNest.sections.push(this.currentSection);
this.currentNest.stage.sections.push(this.currentSection);
}
handleBlockEnd() {
if(this.nesting.length <= 1) {
throw new Error('Invalid block nesting');
}
const subNest = this.nesting.pop();
const {stage, agents} = this.nesting.pop();
this.currentNest = lastElement(this.nesting);
this.currentSection = lastElement(this.currentNest.sections);
if(subNest.agents.length > 0) {
this.addStage(subNest);
this.addColumnBounds(
this.currentNest.agents,
subNest.leftColumn,
subNest.rightColumn,
subNest.agents
);
this.addColumnBounds(
subNest.agents,
subNest.leftColumn,
subNest.rightColumn
this.currentSection = lastElement(this.currentNest.stage.sections);
if(stage.sections.some((section) => section.stages.length > 0)) {
mergeSets(this.currentNest.agents, agents);
mergeSets(this.agents, agents);
this.addBounds(
this.agents,
stage.left,
stage.right,
agents
);
this.currentSection.stages.push(stage);
}
}
handleUnknownStage(stage) {
this.setAgentVis(stage.agents, true, 'box');
this.addStage(stage);
this.currentSection.stages.push(stage);
mergeSets(this.currentNest.agents, stage.agents);
mergeSets(this.agents, stage.agents);
}
handleStage(stage) {
@ -203,6 +205,7 @@ define(() => {
generate({stages, meta = {}}) {
this.agentStates.clear();
this.agents.length = 0;
this.blockCount = 0;
this.nesting.length = 0;
const globals = this.beginNested('global', '', '');
@ -213,19 +216,19 @@ define(() => {
throw new Error('Invalid block nesting');
}
this.setAgentVis(globals.agents, false, meta.terminators || 'none');
this.setAgentVis(this.agents, false, meta.terminators || 'none');
this.addColumnBounds(
globals.agents,
this.currentNest.leftColumn,
this.currentNest.rightColumn
this.addBounds(
this.agents,
this.currentNest.stage.left,
this.currentNest.stage.right
);
return {
meta: {
title: meta.title,
},
agents: globals.agents,
agents: this.agents,
stages: globals.stages,
};
}

View File

@ -51,6 +51,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(sequence.agents).toEqual(['[', 'B', ']']);
});
it('accounts for define calls when ordering agents', () => {
const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: ['B']},
{type: '->', agents: ['A', 'B']},
]});
expect(sequence.agents).toEqual(['[', 'B', 'A', ']']);
});
it('creates implicit begin stages for agents when used', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
@ -216,7 +224,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('records virtual block column names in blocks', () => {
it('records virtual block agent names in blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
@ -225,23 +233,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const block0 = sequence.stages[0];
expect(block0.type).toEqual('block');
expect(block0.leftColumn).toEqual('__BLOCK0[');
expect(block0.rightColumn).toEqual('__BLOCK0]');
});
it('records all involved agents in blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'C']},
{type: BLOCK_END},
]});
const block0 = sequence.stages[0];
expect(block0.agents).toEqual(
['__BLOCK0[', 'A', 'B', 'C', '__BLOCK0]']
);
expect(block0.left).toEqual('__BLOCK0[');
expect(block0.right).toEqual('__BLOCK0]');
});
it('records all sections within blocks', () => {
@ -277,9 +270,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: BLOCK_END},
]});
const block0 = sequence.stages[0];
expect(block0.type).toEqual('block');
expect(block0.agents).toEqual([
expect(sequence.agents).toEqual([
'[',
'__BLOCK0[',
'__BLOCK1[',
'A',
@ -287,20 +279,48 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
'C',
'__BLOCK1]',
'__BLOCK0]',
']',
]);
expect(block0.leftColumn).toEqual('__BLOCK0[');
expect(block0.rightColumn).toEqual('__BLOCK0]');
const block0 = sequence.stages[0];
expect(block0.type).toEqual('block');
expect(block0.left).toEqual('__BLOCK0[');
expect(block0.right).toEqual('__BLOCK0]');
const block1 = block0.sections[1].stages[0];
expect(block1.type).toEqual('block');
expect(block1.agents).toEqual([
expect(block1.left).toEqual('__BLOCK1[');
expect(block1.right).toEqual('__BLOCK1]');
});
it('preserves block boundaries when agents exist outside', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'B']},
{type: BLOCK_END},
{type: BLOCK_END},
]});
expect(sequence.agents).toEqual([
'[',
'__BLOCK0[',
'__BLOCK1[',
'C',
'A',
'B',
'__BLOCK1]',
'__BLOCK0]',
']',
]);
expect(block1.leftColumn).toEqual('__BLOCK1[');
expect(block1.rightColumn).toEqual('__BLOCK1]');
const block0 = sequence.stages[2];
expect(block0.type).toEqual('block');
expect(block0.left).toEqual('__BLOCK0[');
expect(block0.right).toEqual('__BLOCK0]');
const block1 = block0.sections[0].stages[0];
expect(block1.type).toEqual('block');
expect(block1.left).toEqual('__BLOCK1[');
expect(block1.right).toEqual('__BLOCK1]');
});
it('allows empty block parts after split', () => {
@ -351,6 +371,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(sequence.stages).toEqual([]);
});
it('removes blocks which only contain define statements', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: AGENT_DEFINE, agents: ['A']},
{type: BLOCK_END},
]});
expect(sequence.stages).toEqual([]);
});
it('does not create virtual agents for empty blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},

View File

@ -48,8 +48,32 @@ define(() => {
const CONNECT_POINT = 4;
const CONNECT_LABEL_PADDING = 6;
const CONNECT_LABEL_MASK_PADDING = 3;
const CONNECT_LABEL_MARGIN_TOP = 2;
const CONNECT_LABEL_MARGIN_BOTTOM = 1;
const CONNECT_LABEL_MARGIN = {
top: 2,
bottom: 1,
};
const BLOCK_MARGIN = {
top: 5,
bottom: 5,
};
const BLOCK_SECTION_PADDING = {
top: 1,
bottom: 5,
};
const BLOCK_MODE_PADDING = {
top: 1,
left: 3,
right: 3,
bottom: 0,
};
const BLOCK_LABEL_PADDING = {
left: 5,
right: 5,
};
const BLOCK_LABEL_MASK_PADDING = {
left: 3,
right: 3,
};
const ATTRS = {
TITLE: {
@ -85,6 +109,40 @@ define(() => {
'height': 5,
},
BLOCK_BOX: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
},
BLOCK_SEPARATOR: {
'stroke': '#000000',
'stroke-width': 1.5,
'stroke-dasharray': '4, 2',
},
BLOCK_MODE: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 2,
'ry': 2,
},
BLOCK_MODE_LABEL: {
'font-family': 'sans-serif',
'font-weight': 'bold',
'font-size': 9,
'text-anchor': 'left',
},
BLOCK_LABEL: {
'font-family': 'sans-serif',
'font-size': 8,
'text-anchor': 'left',
},
BLOCK_LABEL_MASK: {
'fill': '#FFFFFF',
},
CONNECT_LINE_SOLID: {
'fill': 'none',
'stroke': '#000000',
@ -94,7 +152,7 @@ define(() => {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'stroke-dasharray': '2, 2',
'stroke-dasharray': '4, 2',
},
CONNECT_LABEL: {
'font-family': 'sans-serif',
@ -126,13 +184,27 @@ define(() => {
return o;
}
function traverse(stages, fn) {
function traverse(stages, callbacks) {
stages.forEach((stage) => {
fn(stage);
if(stage.type === 'block') {
const scope = {};
if(callbacks.blockBeginFn) {
callbacks.blockBeginFn(scope, stage);
}
stage.sections.forEach((section) => {
traverse(section.stages, fn);
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.stageFn) {
callbacks.stageFn(stage);
}
});
}
@ -155,9 +227,13 @@ define(() => {
this.diagram = makeSVGNode('g');
this.agentLines = makeSVGNode('g');
this.blocks = makeSVGNode('g');
this.sections = makeSVGNode('g');
this.agentDecor = makeSVGNode('g');
this.actions = makeSVGNode('g');
this.diagram.appendChild(this.agentLines);
this.diagram.appendChild(this.blocks);
this.diagram.appendChild(this.sections);
this.diagram.appendChild(this.agentDecor);
this.diagram.appendChild(this.actions);
this.base.appendChild(this.diagram);
@ -173,7 +249,6 @@ define(() => {
'agent begin': this.separationAgent.bind(this),
'agent end': this.separationAgent.bind(this),
'connection': this.separationConnection.bind(this),
'block': this.separationBlock.bind(this),
'note over': this.separationNoteOver.bind(this),
'note left': this.separationNoteLeft.bind(this),
'note right': this.separationNoteRight.bind(this),
@ -191,13 +266,27 @@ define(() => {
'agent begin': this.renderAgentBegin.bind(this),
'agent end': this.renderAgentEnd.bind(this),
'connection': this.renderConnection.bind(this),
'block': this.renderBlock.bind(this),
'note over': this.renderNoteOver.bind(this),
'note left': this.renderNoteLeft.bind(this),
'note right': this.renderNoteRight.bind(this),
'note between': this.renderNoteBetween.bind(this),
};
this.separationTraversalFns = {
stageFn: this.checkSeparation.bind(this),
blockBeginFn: this.separationBlockBegin.bind(this),
sectionBeginFn: this.separationSectionBegin.bind(this),
blockEndFn: this.separationBlockEnd.bind(this),
};
this.renderTraversalFns = {
stageFn: this.addAction.bind(this),
blockBeginFn: this.renderBlockBegin.bind(this),
sectionBeginFn: this.renderSectionBegin.bind(this),
sectionEndFn: this.renderSectionEnd.bind(this),
blockEndFn: this.renderBlockEnd.bind(this),
};
this.width = 0;
this.height = 0;
}
@ -280,17 +369,13 @@ define(() => {
agents[0],
agents[1],
this.testTextWidth(this.testConnectWidth, label) +
this.testTextWidth(this.testConnect, label) +
CONNECT_POINT * 2 +
CONNECT_LABEL_PADDING * 2 +
ATTRS.AGENT_LINE['stroke-width']
);
}
separationBlock(/*stage*/) {
// TODO
}
separationNoteOver(/*stage*/) {
// TODO
}
@ -307,6 +392,25 @@ define(() => {
// TODO
}
separationBlockBegin(scope, {left, right}) {
mergeSets(this.visibleAgents, [left, right]);
this.addSeparations(this.visibleAgents, new Map());
}
separationSectionBegin(scope, {left, right}, {mode, label}) {
const width = (
this.testTextWidth(this.testBlockMode, mode) +
BLOCK_MODE_PADDING.left + BLOCK_MODE_PADDING.right +
this.testTextWidth(this.testBlockLabel, label) +
BLOCK_LABEL_PADDING.left + BLOCK_LABEL_PADDING.right
);
this.addSeparation(left, right, width);
}
separationBlockEnd(scope, {left, right}) {
removeAll(this.visibleAgents, [left, right]);
}
checkSeparation(stage) {
this.separationAction[stage.type](stage);
}
@ -430,16 +534,16 @@ define(() => {
this.actions.appendChild(labelNode);
y += Math.max(
dy,
CONNECT_LABEL_MARGIN_TOP +
CONNECT_LABEL_MARGIN.top +
sz * LINE_HEIGHT +
CONNECT_LABEL_MARGIN_BOTTOM
CONNECT_LABEL_MARGIN.bottom
);
const w = labelNode.getComputedTextLength();
const x = (from.x + to.x) / 2;
const yBase = (
y -
sz * (LINE_HEIGHT - 1) -
CONNECT_LABEL_MARGIN_BOTTOM
CONNECT_LABEL_MARGIN.bottom
);
labelNode.setAttribute('x', x);
labelNode.setAttribute('y', yBase);
@ -483,10 +587,6 @@ define(() => {
this.currentY = y + dy + ACTION_MARGIN;
}
renderBlock(/*stage*/) {
// TODO
}
renderNoteOver(/*stage*/) {
// TODO
}
@ -503,6 +603,106 @@ define(() => {
// TODO
}
renderBlockBegin(scope) {
this.currentY += BLOCK_MARGIN.top;
scope.y = this.currentY;
scope.first = true;
}
renderSectionBegin(scope, {left, right}, {mode, label}) {
const agentInfoL = this.agentInfos.get(left);
const agentInfoR = this.agentInfos.get(right);
if(scope.first) {
scope.first = false;
} else {
this.currentY += BLOCK_SECTION_PADDING.bottom;
this.sections.appendChild(makeSVGNode('path', Object.assign({
'd': (
'M' + agentInfoL.x + ' ' + this.currentY +
' L' + agentInfoR.x + ' ' + this.currentY
),
}, ATTRS.BLOCK_SEPARATOR)));
}
let x = agentInfoL.x;
if(mode) {
const sz = ATTRS.BLOCK_MODE_LABEL['font-size'];
const modeBox = makeSVGNode('rect', Object.assign({
'x': x,
'y': this.currentY,
'height': (
sz * LINE_HEIGHT +
BLOCK_MODE_PADDING.top +
BLOCK_MODE_PADDING.bottom
),
}, ATTRS.BLOCK_MODE));
const modeLabel = makeSVGNode('text', Object.assign({
'x': x + BLOCK_MODE_PADDING.left,
'y': (
this.currentY + sz +
BLOCK_MODE_PADDING.top
),
}, ATTRS.BLOCK_MODE_LABEL));
modeLabel.appendChild(makeText(mode));
this.blocks.appendChild(modeBox);
this.actions.appendChild(modeLabel);
const w = (
modeLabel.getComputedTextLength() +
BLOCK_MODE_PADDING.left +
BLOCK_MODE_PADDING.right
);
modeBox.setAttribute('width', w);
x += w;
this.currentY += sz * LINE_HEIGHT;
}
if(label) {
x += BLOCK_LABEL_PADDING.left;
const sz = ATTRS.BLOCK_LABEL['font-size'];
const mask = makeSVGNode('rect', Object.assign({
'x': x - BLOCK_LABEL_MASK_PADDING.left,
'y': this.currentY - sz * LINE_HEIGHT,
'height': sz * LINE_HEIGHT,
}, ATTRS.BLOCK_LABEL_MASK));
const labelLabel = makeSVGNode('text', Object.assign({
'x': x,
'y': this.currentY - sz * (LINE_HEIGHT - 1),
}, ATTRS.BLOCK_LABEL));
labelLabel.appendChild(makeText(label));
this.actions.appendChild(mask);
this.actions.appendChild(labelLabel);
const w = (
labelLabel.getComputedTextLength() +
BLOCK_LABEL_MASK_PADDING.left +
BLOCK_LABEL_MASK_PADDING.right
);
mask.setAttribute('width', w);
}
this.currentY += BLOCK_SECTION_PADDING.top;
}
renderSectionEnd(/*scope, block, section*/) {
}
renderBlockEnd(scope, {left, right}) {
this.currentY += BLOCK_SECTION_PADDING.bottom;
const agentInfoL = this.agentInfos.get(left);
const agentInfoR = this.agentInfos.get(right);
this.blocks.appendChild(makeSVGNode('rect', Object.assign({
'x': agentInfoL.x,
'y': scope.y,
'width': agentInfoR.x - agentInfoL.x,
'height': this.currentY - scope.y,
}, ATTRS.BLOCK_BOX)));
this.currentY += BLOCK_MARGIN.bottom;
}
addAction(stage) {
this.renderAction[stage.type](stage);
}
@ -525,14 +725,14 @@ define(() => {
}
buildAgentInfos(agents, stages) {
const testNameWidth = this.makeTextTester(ATTRS.AGENT_BOX_LABEL);
const testName = this.makeTextTester(ATTRS.AGENT_BOX_LABEL);
this.agentInfos = new Map();
agents.forEach((agent, index) => {
this.agentInfos.set(agent, {
label: agent,
labelWidth: (
this.testTextWidth(testNameWidth, agent) +
this.testTextWidth(testName, agent) +
BOX_PADDING * 2
),
index,
@ -544,12 +744,16 @@ define(() => {
this.agentInfos.get('[').labelWidth = 0;
this.agentInfos.get(']').labelWidth = 0;
this.removeTextTester(testNameWidth);
this.removeTextTester(testName);
this.testConnectWidth = this.makeTextTester(ATTRS.CONNECT_LABEL);
this.testConnect = this.makeTextTester(ATTRS.CONNECT_LABEL);
this.testBlockMode = this.makeTextTester(ATTRS.BLOCK_MODE_LABEL);
this.testBlockLabel = this.makeTextTester(ATTRS.BLOCK_LABEL);
this.visibleAgents = ['[', ']'];
traverse(stages, this.checkSeparation.bind(this));
this.removeTextTester(this.testConnectWidth);
traverse(stages, this.separationTraversalFns);
this.removeTextTester(this.testConnect);
this.removeTextTester(this.testBlockMode);
this.removeTextTester(this.testBlockLabel);
agents.forEach((agent) => {
const agentInfo = this.agentInfos.get(agent);
@ -597,6 +801,8 @@ define(() => {
render({meta, agents, stages}) {
empty(this.agentLines);
empty(this.blocks);
empty(this.sections);
empty(this.agentDecor);
empty(this.actions);
@ -607,12 +813,12 @@ define(() => {
this.buildAgentInfos(agents, stages);
this.currentY = 0;
traverse(stages, this.addAction.bind(this));
traverse(stages, this.renderTraversalFns);
this.updateBounds(Math.max(this.currentY - ACTION_MARGIN, 0));
}
getColumnX(name) {
getAgentX(name) {
return this.agentInfos.get(name).x;
}

View File

@ -42,7 +42,7 @@ defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => {
expect(title.innerHTML).toEqual('Title');
});
it('positions column lines', () => {
it('positions agent lines', () => {
/*
A -> B
*/
@ -61,10 +61,10 @@ defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => {
const line = element.getElementsByClassName('agent-1-line')[0];
const drawnX = Number(line.getAttribute('d').split(' ')[1]);
expect(drawnX).toEqual(renderer.getColumnX('A'));
expect(drawnX).toEqual(renderer.getAgentX('A'));
});
it('arranges columns left-to-right', () => {
it('arranges agents left-to-right', () => {
/*
[ -> A
A -> B
@ -85,11 +85,11 @@ defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => {
],
});
const xL = renderer.getColumnX('[');
const xA = renderer.getColumnX('A');
const xB = renderer.getColumnX('B');
const xC = renderer.getColumnX('C');
const xR = renderer.getColumnX(']');
const xL = renderer.getAgentX('[');
const xA = renderer.getAgentX('A');
const xB = renderer.getAgentX('B');
const xC = renderer.getAgentX('C');
const xR = renderer.getAgentX(']');
expect(xA).toBeGreaterThan(xL);
expect(xB).toBeGreaterThan(xA);
@ -97,7 +97,7 @@ defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => {
expect(xR).toBeGreaterThan(xC);
});
it('allows column reordering for mutually-exclusive columns', () => {
it('allows agent reordering for mutually-exclusive agents', () => {
/*
A -> B: short
end B
@ -123,10 +123,10 @@ defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => {
],
});
const xA = renderer.getColumnX('A');
const xB = renderer.getColumnX('B');
const xC = renderer.getColumnX('C');
const xD = renderer.getColumnX('D');
const xA = renderer.getAgentX('A');
const xB = renderer.getAgentX('B');
const xC = renderer.getAgentX('C');
const xD = renderer.getAgentX('D');
expect(xB).toBeGreaterThan(xA);
expect(xC).toBeGreaterThan(xA);