Split some minor functionality out of Renderer (beginnings of code cleanup)

This commit is contained in:
David Evans 2017-10-25 21:45:21 +01:00
parent b240990f3e
commit c6bc372688
7 changed files with 493 additions and 291 deletions

View File

@ -0,0 +1,44 @@
define(() => {
'use strict';
function mergeSets(target, b = null) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
if(target.indexOf(b[i]) === -1) {
target.push(b[i]);
}
}
}
function removeAll(target, b = null) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
const p = target.indexOf(b[i]);
if(p !== -1) {
target.splice(p, 1);
}
}
}
function remove(list, item) {
const p = list.indexOf(item);
if(p !== -1) {
list.splice(p, 1);
}
}
function last(list) {
return list[list.length - 1];
}
return {
mergeSets,
removeAll,
remove,
last,
};
});

View File

@ -0,0 +1,111 @@
defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => {
'use strict';
describe('.mergeSets', () => {
it('adds elements from the second array into the first', () => {
const p1 = ['a', 'b'];
const p2 = ['c', 'd'];
array.mergeSets(p1, p2);
expect(p1).toEqual(['a', 'b', 'c', 'd']);
});
it('ignores null parameters', () => {
const p1 = ['a', 'b'];
array.mergeSets(p1, null);
expect(p1).toEqual(['a', 'b']);
});
it('leaves the second parameter unchanged', () => {
const p1 = ['a', 'b'];
const p2 = ['c', 'd'];
array.mergeSets(p1, p2);
expect(p2).toEqual(['c', 'd']);
});
it('ignores duplicates', () => {
const p1 = ['a', 'b'];
const p2 = ['b', 'c'];
array.mergeSets(p1, p2);
expect(p1).toEqual(['a', 'b', 'c']);
});
it('maintains input ordering', () => {
const p1 = ['a', 'x', 'c', 'd'];
const p2 = ['d', 'x', 'e', 'a'];
array.mergeSets(p1, p2);
expect(p1).toEqual(['a', 'x', 'c', 'd', 'e']);
});
});
describe('.removeAll', () => {
it('removes elements from the first array', () => {
const p1 = ['a', 'b', 'c'];
const p2 = ['a', 'b'];
array.removeAll(p1, p2);
expect(p1).toEqual(['c']);
});
it('ignores null parameters', () => {
const p1 = ['a', 'b'];
array.removeAll(p1, null);
expect(p1).toEqual(['a', 'b']);
});
it('leaves the second parameter unchanged', () => {
const p1 = ['a', 'b', 'c'];
const p2 = ['a', 'b'];
array.removeAll(p1, p2);
expect(p2).toEqual(['a', 'b']);
});
it('ignores duplicates', () => {
const p1 = ['a', 'b', 'c'];
const p2 = ['a', 'b', 'b'];
array.removeAll(p1, p2);
expect(p1).toEqual(['c']);
});
it('maintains input ordering', () => {
const p1 = ['a', 'x', 'c', 'd'];
const p2 = ['c'];
array.removeAll(p1, p2);
expect(p1).toEqual(['a', 'x', 'd']);
});
});
describe('.remove', () => {
it('removes one element matching the parameter', () => {
const p1 = ['a', 'b'];
array.removeAll(p1, 'b');
expect(p1).toEqual(['a']);
});
it('removes only the first element matching the parameter', () => {
const p1 = ['a', 'b', 'c', 'b'];
array.removeAll(p1, 'b');
expect(p1).toEqual(['a', 'c', 'b']);
});
it('ignores if not found', () => {
const p1 = ['a', 'b', 'c'];
array.removeAll(p1, 'nope');
expect(p1).toEqual(['a', 'b', 'c']);
});
it('maintains input ordering', () => {
const p1 = ['a', 'b', 'c'];
array.removeAll(p1, 'b');
expect(p1).toEqual(['a', 'c']);
});
});
describe('.last', () => {
it('returns the last element of the array', () => {
expect(array.last(['a', 'b'])).toEqual('b');
});
it('returns undefined for empty arrays', () => {
expect(array.last([])).toEqual(undefined);
});
});
});

View File

@ -1,28 +1,6 @@
define(() => { define(['./ArrayUtilities'], (array) => {
'use strict'; 'use strict';
function mergeSets(target, b) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
if(target.indexOf(b[i]) === -1) {
target.push(b[i]);
}
}
}
function removeElement(list, item) {
const p = list.indexOf(item);
if(p !== -1) {
list.splice(p, 1);
}
}
function lastElement(list) {
return list[list.length - 1];
}
class AgentState { class AgentState {
constructor(visible, locked = false) { constructor(visible, locked = false) {
this.visible = visible; this.visible = visible;
@ -54,8 +32,8 @@ define(() => {
} }
addBounds(target, agentL, agentR, involvedAgents = null) { addBounds(target, agentL, agentR, involvedAgents = null) {
removeElement(target, agentL); array.remove(target, agentL);
removeElement(target, agentR); array.remove(target, agentR);
let indexL = 0; let indexL = 0;
let indexR = target.length; let indexR = target.length;
@ -96,9 +74,9 @@ define(() => {
} }
}); });
const type = (visible ? 'agent begin' : 'agent end'); const type = (visible ? 'agent begin' : 'agent end');
const existing = lastElement(this.currentSection.stages) || {}; const existing = array.last(this.currentSection.stages) || {};
if(existing.type === type && existing.mode === mode) { if(existing.type === type && existing.mode === mode) {
mergeSets(existing.agents, filteredAgents); array.mergeSets(existing.agents, filteredAgents);
} else { } else {
this.currentSection.stages.push({ this.currentSection.stages.push({
type, type,
@ -106,8 +84,8 @@ define(() => {
mode, mode,
}); });
} }
mergeSets(this.currentNest.agents, filteredAgents); array.mergeSets(this.currentNest.agents, filteredAgents);
mergeSets(this.agents, filteredAgents); array.mergeSets(this.agents, filteredAgents);
} }
beginNested(mode, label, name) { beginNested(mode, label, name) {
@ -137,8 +115,8 @@ define(() => {
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
mergeSets(this.currentNest.agents, agents); array.mergeSets(this.currentNest.agents, agents);
mergeSets(this.agents, agents); array.mergeSets(this.agents, agents);
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode}) {
@ -172,11 +150,11 @@ define(() => {
throw new Error('Invalid block nesting'); throw new Error('Invalid block nesting');
} }
const {stage, agents} = this.nesting.pop(); const {stage, agents} = this.nesting.pop();
this.currentNest = lastElement(this.nesting); this.currentNest = array.last(this.nesting);
this.currentSection = lastElement(this.currentNest.stage.sections); this.currentSection = array.last(this.currentNest.stage.sections);
if(stage.sections.some((section) => section.stages.length > 0)) { if(stage.sections.some((section) => section.stages.length > 0)) {
mergeSets(this.currentNest.agents, agents); array.mergeSets(this.currentNest.agents, agents);
mergeSets(this.agents, agents); array.mergeSets(this.agents, agents);
this.addBounds( this.addBounds(
this.agents, this.agents,
stage.left, stage.left,
@ -190,8 +168,8 @@ define(() => {
handleUnknownStage(stage) { handleUnknownStage(stage) {
this.setAgentVis(stage.agents, true, 'box'); this.setAgentVis(stage.agents, true, 'box');
this.currentSection.stages.push(stage); this.currentSection.stages.push(stage);
mergeSets(this.currentNest.agents, stage.agents); array.mergeSets(this.currentNest.agents, stage.agents);
mergeSets(this.agents, stage.agents); array.mergeSets(this.agents, stage.agents);
} }
handleStage(stage) { handleStage(stage) {

View File

@ -1,66 +1,19 @@
define(() => { define(['./ArrayUtilities', './SVGUtilities'], (array, svg) => {
'use strict'; 'use strict';
/* jshint -W071 */ // TODO: break up rendering logic
const NS = 'http://www.w3.org/2000/svg';
function makeText(text = '') {
return document.createTextNode(text);
}
function makeSVGNode(type, attrs = {}) {
const o = document.createElementNS(NS, type);
for(let k in attrs) {
if(attrs.hasOwnProperty(k)) {
o.setAttribute(k, attrs[k]);
}
}
return o;
}
function empty(node) {
while(node.childNodes.length > 0) {
node.removeChild(node.lastChild);
}
}
function mergeSets(target, b) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
if(target.indexOf(b[i]) === -1) {
target.push(b[i]);
}
}
}
function removeAll(target, b) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
const p = target.indexOf(b[i]);
if(p !== -1) {
target.splice(p, 1);
}
}
}
function boxRenderer(attrs, position) { function boxRenderer(attrs, position) {
return makeSVGNode('rect', Object.assign({}, position, attrs)); return svg.make('rect', Object.assign({}, position, attrs));
} }
function noteRenderer(attrs, flickAttrs, position) { function noteRenderer(attrs, flickAttrs, position) {
const g = makeSVGNode('g'); const g = svg.make('g');
const x0 = position.x; const x0 = position.x;
const x1 = position.x + position.width; const x1 = position.x + position.width;
const y0 = position.y; const y0 = position.y;
const y1 = position.y + position.height; const y1 = position.y + position.height;
const flick = 7; const flick = 7;
g.appendChild(makeSVGNode('path', Object.assign({ g.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + x0 + ' ' + y0 + 'M ' + x0 + ' ' + y0 +
' L ' + (x1 - flick) + ' ' + y0 + ' L ' + (x1 - flick) + ' ' + y0 +
@ -71,7 +24,7 @@ define(() => {
), ),
}, attrs))); }, attrs)));
g.appendChild(makeSVGNode('path', Object.assign({ g.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + (x1 - flick) + ' ' + y0 + 'M ' + (x1 - flick) + ' ' + y0 +
' L ' + (x1 - flick) + ' ' + (y0 + flick) + ' L ' + (x1 - flick) + ' ' + (y0 + flick) +
@ -87,40 +40,117 @@ define(() => {
const LINE_HEIGHT = 1.3; const LINE_HEIGHT = 1.3;
const TITLE_MARGIN = 10; const TITLE_MARGIN = 10;
const OUTER_MARGIN = 5; const OUTER_MARGIN = 5;
const BOX_PADDING = 10; const AGENT_BOX_PADDING = 10;
const AGENT_MARGIN = 10; const AGENT_MARGIN = 10;
const AGENT_CROSS_SIZE = 20; const AGENT_CROSS_SIZE = 20;
const AGENT_NONE_HEIGHT = 10; const AGENT_NONE_HEIGHT = 10;
const ACTION_MARGIN = 5; const ACTION_MARGIN = 5;
const CONNECT_HEIGHT = 8;
const CONNECT_POINT = 4; const CONNECT = {
const CONNECT_LABEL_PADDING = 6; lineAttrs: {
const CONNECT_LABEL_MASK_PADDING = 3; 'solid': {
const CONNECT_LABEL_MARGIN = { 'fill': 'none',
top: 2, 'stroke': '#000000',
bottom: 1, 'stroke-width': 1,
},
'dash': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'stroke-dasharray': '4, 2',
},
},
arrow: {
width: 4,
height: 8,
attrs: {
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
'stroke-linejoin': 'miter',
},
},
label: {
padding: 6,
margin: {top: 2, bottom: 1},
attrs: {
'font-family': 'sans-serif',
'font-size': 8,
'text-anchor': 'middle',
},
},
mask: {
padding: 3,
attrs: {
'fill': '#FFFFFF',
},
},
}; };
const BLOCK_MARGIN = {
top: 0, const BLOCK = {
bottom: 0, margin: {
}; top: 0,
const BLOCK_SECTION_PADDING = { bottom: 0,
top: 3, },
bottom: 2, boxAttrs: {
}; 'fill': 'none',
const BLOCK_MODE_PADDING = { 'stroke': '#000000',
top: 1, 'stroke-width': 1.5,
left: 3, 'rx': 2,
right: 3, 'ry': 2,
bottom: 0, },
}; section: {
const BLOCK_LABEL_PADDING = { padding: {
left: 5, top: 3,
right: 5, bottom: 2,
}; },
const BLOCK_LABEL_MASK_PADDING = { mode: {
left: 3, padding: {
right: 3, top: 1,
left: 3,
right: 3,
bottom: 0,
},
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 2,
'ry': 2,
},
labelAttrs: {
'font-family': 'sans-serif',
'font-weight': 'bold',
'font-size': 9,
'text-anchor': 'left',
},
},
label: {
maskPadding: {
left: 3,
right: 3,
},
maskAttrs: {
'fill': '#FFFFFF',
},
labelPadding: {
left: 5,
right: 5,
},
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'text-anchor': 'left',
},
},
},
separator: {
attrs: {
'stroke': '#000000',
'stroke-width': 1.5,
'stroke-dasharray': '4, 2',
},
},
}; };
const NOTE = { const NOTE = {
@ -193,66 +223,6 @@ define(() => {
'fill': '#000000', 'fill': '#000000',
'height': 5, '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',
'stroke-width': 1,
},
CONNECT_LINE_DASH: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'stroke-dasharray': '4, 2',
},
CONNECT_LABEL: {
'font-family': 'sans-serif',
'font-size': 8,
'text-anchor': 'middle',
},
CONNECT_LABEL_MASK: {
'fill': '#FFFFFF',
},
CONNECT_HEAD: {
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
'stroke-linejoin': 'miter',
},
}; };
function traverse(stages, callbacks) { function traverse(stages, callbacks) {
@ -282,36 +252,6 @@ define(() => {
return class Renderer { return class Renderer {
constructor() { constructor() {
this.base = makeSVGNode('svg', {
'xmlns': NS,
'version': '1.1',
'width': '100%',
'height': '100%',
});
this.title = makeSVGNode('text', Object.assign({
'y': ATTRS.TITLE['font-size'] + OUTER_MARGIN,
}, ATTRS.TITLE));
this.titleText = makeText();
this.title.appendChild(this.titleText);
this.base.appendChild(this.title);
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);
this.testers = makeSVGNode('g');
this.testersCache = new Map();
this.separationAgentCap = { this.separationAgentCap = {
'box': this.separationAgentCapBox.bind(this), 'box': this.separationAgentCapBox.bind(this),
'cross': this.separationAgentCapCross.bind(this), 'cross': this.separationAgentCapCross.bind(this),
@ -363,6 +303,37 @@ define(() => {
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.buildStaticElements();
}
buildStaticElements() {
this.base = svg.makeContainer({
'width': '100%',
'height': '100%',
});
this.title = svg.make('text', Object.assign({
'y': ATTRS.TITLE['font-size'] + OUTER_MARGIN,
}, ATTRS.TITLE));
this.titleText = svg.makeText();
this.title.appendChild(this.titleText);
this.base.appendChild(this.title);
this.diagram = svg.make('g');
this.agentLines = svg.make('g');
this.blocks = svg.make('g');
this.sections = svg.make('g');
this.agentDecor = svg.make('g');
this.actions = svg.make('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);
this.testers = svg.make('g');
this.testersCache = new Map();
} }
findExtremes(agents) { findExtremes(agents) {
@ -440,7 +411,7 @@ define(() => {
separationAgent({type, mode, agents}) { separationAgent({type, mode, agents}) {
if(type === 'agent begin') { if(type === 'agent begin') {
mergeSets(this.visibleAgents, agents); array.mergeSets(this.visibleAgents, agents);
} }
const agentSpaces = new Map(); const agentSpaces = new Map();
@ -452,7 +423,7 @@ define(() => {
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
if(type === 'agent end') { if(type === 'agent end') {
removeAll(this.visibleAgents, agents); array.removeAll(this.visibleAgents, agents);
} }
} }
@ -461,9 +432,9 @@ define(() => {
agents[0], agents[0],
agents[1], agents[1],
this.testTextWidth(ATTRS.CONNECT_LABEL, label) + this.testTextWidth(CONNECT.label.attrs, label) +
CONNECT_POINT * 2 + CONNECT.arrow.width * 2 +
CONNECT_LABEL_PADDING * 2 + CONNECT.label.padding * 2 +
ATTRS.AGENT_LINE['stroke-width'] ATTRS.AGENT_LINE['stroke-width']
); );
} }
@ -553,22 +524,24 @@ define(() => {
} }
separationBlockBegin(scope, {left, right}) { separationBlockBegin(scope, {left, right}) {
mergeSets(this.visibleAgents, [left, right]); array.mergeSets(this.visibleAgents, [left, right]);
this.addSeparations(this.visibleAgents, new Map()); this.addSeparations(this.visibleAgents, new Map());
} }
separationSectionBegin(scope, {left, right}, {mode, label}) { separationSectionBegin(scope, {left, right}, {mode, label}) {
const width = ( const width = (
this.testTextWidth(ATTRS.BLOCK_MODE_LABEL, mode) + this.testTextWidth(BLOCK.section.mode.labelAttrs, mode) +
BLOCK_MODE_PADDING.left + BLOCK_MODE_PADDING.right + BLOCK.section.mode.padding.left +
this.testTextWidth(ATTRS.BLOCK_LABEL, label) + BLOCK.section.mode.padding.right +
BLOCK_LABEL_PADDING.left + BLOCK_LABEL_PADDING.right this.testTextWidth(BLOCK.section.label.labelAttrs, label) +
BLOCK.section.label.labelPadding.left +
BLOCK.section.label.labelPadding.right
); );
this.addSeparation(left, right, width); this.addSeparation(left, right, width);
} }
separationBlockEnd(scope, {left, right}) { separationBlockEnd(scope, {left, right}) {
removeAll(this.visibleAgents, [left, right]); array.removeAll(this.visibleAgents, [left, right]);
} }
checkSeparation(stage) { checkSeparation(stage) {
@ -576,20 +549,20 @@ define(() => {
} }
renderAgentCapBox({x, labelWidth, label}) { renderAgentCapBox({x, labelWidth, label}) {
this.agentDecor.appendChild(makeSVGNode('rect', Object.assign({ this.agentDecor.appendChild(svg.make('rect', Object.assign({
'x': x - labelWidth / 2, 'x': x - labelWidth / 2,
'y': this.currentY, 'y': this.currentY,
'width': labelWidth, 'width': labelWidth,
}, ATTRS.AGENT_BOX))); }, ATTRS.AGENT_BOX)));
const name = makeSVGNode('text', Object.assign({ const name = svg.make('text', Object.assign({
'x': x, 'x': x,
'y': this.currentY + ( 'y': this.currentY + (
ATTRS.AGENT_BOX.height + ATTRS.AGENT_BOX.height +
ATTRS.AGENT_BOX_LABEL['font-size'] * (2 - LINE_HEIGHT) ATTRS.AGENT_BOX_LABEL['font-size'] * (2 - LINE_HEIGHT)
) / 2, ) / 2,
}, ATTRS.AGENT_BOX_LABEL)); }, ATTRS.AGENT_BOX_LABEL));
name.appendChild(makeText(label)); name.appendChild(svg.makeText(label));
this.agentDecor.appendChild(name); this.agentDecor.appendChild(name);
return { return {
@ -603,7 +576,7 @@ define(() => {
const y = this.currentY; const y = this.currentY;
const d = AGENT_CROSS_SIZE / 2; const d = AGENT_CROSS_SIZE / 2;
this.agentDecor.appendChild(makeSVGNode('path', Object.assign({ this.agentDecor.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + (x - d) + ' ' + y + 'M ' + (x - d) + ' ' + y +
' L ' + (x + d) + ' ' + (y + d * 2) + ' L ' + (x + d) + ' ' + (y + d * 2) +
@ -620,7 +593,7 @@ define(() => {
} }
renderAgentCapBar({x, labelWidth}) { renderAgentCapBar({x, labelWidth}) {
this.agentDecor.appendChild(makeSVGNode('rect', Object.assign({ this.agentDecor.appendChild(svg.make('rect', Object.assign({
'x': x - labelWidth / 2, 'x': x - labelWidth / 2,
'y': this.currentY, 'y': this.currentY,
'width': labelWidth, 'width': labelWidth,
@ -657,7 +630,7 @@ define(() => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(agent);
const x = agentInfo.x; const x = agentInfo.x;
shifts = this.renderAgentCap[mode](agentInfo); shifts = this.renderAgentCap[mode](agentInfo);
this.agentLines.appendChild(makeSVGNode('path', Object.assign({ this.agentLines.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + x + ' ' + agentInfo.latestYStart + 'M ' + x + ' ' + agentInfo.latestYStart +
' L ' + x + ' ' + (this.currentY + shifts.lineTop) ' L ' + x + ' ' + (this.currentY + shifts.lineTop)
@ -674,74 +647,69 @@ define(() => {
const from = this.agentInfos.get(agents[0]); const from = this.agentInfos.get(agents[0]);
const to = this.agentInfos.get(agents[1]); const to = this.agentInfos.get(agents[1]);
const dy = CONNECT_HEIGHT / 2; const dy = CONNECT.arrow.height / 2;
const dx = CONNECT_POINT; const dx = CONNECT.arrow.width;
const dir = (from.x < to.x) ? 1 : -1; const dir = (from.x < to.x) ? 1 : -1;
const short = ATTRS.AGENT_LINE['stroke-width']; const short = ATTRS.AGENT_LINE['stroke-width'];
let y = this.currentY; let y = this.currentY;
const lineAttrs = {
'solid': ATTRS.CONNECT_LINE_SOLID,
'dash': ATTRS.CONNECT_LINE_DASH,
};
if(label) { if(label) {
const mask = makeSVGNode('rect', ATTRS.CONNECT_LABEL_MASK); const mask = svg.make('rect', CONNECT.mask.attrs);
const labelNode = makeSVGNode('text', ATTRS.CONNECT_LABEL); const labelNode = svg.make('text', CONNECT.label.attrs);
labelNode.appendChild(makeText(label)); labelNode.appendChild(svg.makeText(label));
const sz = ATTRS.CONNECT_LABEL['font-size']; const sz = CONNECT.label.attrs['font-size'];
this.actions.appendChild(mask); this.actions.appendChild(mask);
this.actions.appendChild(labelNode); this.actions.appendChild(labelNode);
y += Math.max( y += Math.max(
dy, dy,
CONNECT_LABEL_MARGIN.top + CONNECT.label.margin.top +
sz * LINE_HEIGHT + sz * LINE_HEIGHT +
CONNECT_LABEL_MARGIN.bottom CONNECT.label.margin.bottom
); );
const w = labelNode.getComputedTextLength(); const w = labelNode.getComputedTextLength();
const x = (from.x + to.x) / 2; const x = (from.x + to.x) / 2;
const yBase = ( const yBase = (
y - y -
sz * (LINE_HEIGHT - 1) - sz * (LINE_HEIGHT - 1) -
CONNECT_LABEL_MARGIN.bottom CONNECT.label.margin.bottom
); );
labelNode.setAttribute('x', x); labelNode.setAttribute('x', x);
labelNode.setAttribute('y', yBase); labelNode.setAttribute('y', yBase);
mask.setAttribute('x', x - w / 2 - CONNECT_LABEL_MASK_PADDING); mask.setAttribute('x', x - w / 2 - CONNECT.mask.padding);
mask.setAttribute('y', yBase - sz); mask.setAttribute('y', yBase - sz);
mask.setAttribute('width', w + CONNECT_LABEL_MASK_PADDING * 2); mask.setAttribute('width', w + CONNECT.mask.padding * 2);
mask.setAttribute('height', sz * LINE_HEIGHT); mask.setAttribute('height', sz * LINE_HEIGHT);
} else { } else {
y += dy; y += dy;
} }
this.actions.appendChild(makeSVGNode('path', Object.assign({ this.actions.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + (from.x + (left ? short : 0) * dir) + ' ' + y + 'M ' + (from.x + (left ? short : 0) * dir) + ' ' + y +
' L ' + (to.x - (right ? short : 0) * dir) + ' ' + y ' L ' + (to.x - (right ? short : 0) * dir) + ' ' + y
), ),
}, lineAttrs[line]))); }, CONNECT.lineAttrs[line])));
if(left) { if(left) {
this.actions.appendChild(makeSVGNode('path', Object.assign({ this.actions.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + (from.x + (dx + short) * dir) + ' ' + (y - dy) + 'M ' + (from.x + (dx + short) * dir) + ' ' + (y - dy) +
' L ' + (from.x + short * dir) + ' ' + y + ' L ' + (from.x + short * dir) + ' ' + y +
' L ' + (from.x + (dx + short) * dir) + ' ' + (y + dy) + ' L ' + (from.x + (dx + short) * dir) + ' ' + (y + dy) +
(ATTRS.CONNECT_HEAD.fill === 'none' ? '' : ' Z') (CONNECT.arrow.attrs.fill === 'none' ? '' : ' Z')
), ),
}, ATTRS.CONNECT_HEAD))); }, CONNECT.arrow.attrs)));
} }
if(right) { if(right) {
this.actions.appendChild(makeSVGNode('path', Object.assign({ this.actions.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + (to.x - (dx + short) * dir) + ' ' + (y - dy) + 'M ' + (to.x - (dx + short) * dir) + ' ' + (y - dy) +
' L ' + (to.x - short * dir) + ' ' + y + ' L ' + (to.x - short * dir) + ' ' + y +
' L ' + (to.x - (dx + short) * dir) + ' ' + (y + dy) + ' L ' + (to.x - (dx + short) * dir) + ' ' + (y + dy) +
(ATTRS.CONNECT_HEAD.fill === 'none' ? '' : ' Z') (CONNECT.arrow.attrs.fill === 'none' ? '' : ' Z')
), ),
}, ATTRS.CONNECT_HEAD))); }, CONNECT.arrow.attrs)));
} }
this.currentY = y + dy + ACTION_MARGIN; this.currentY = y + dy + ACTION_MARGIN;
@ -754,11 +722,11 @@ define(() => {
this.currentY += config.margin.top; this.currentY += config.margin.top;
const labelNode = makeSVGNode('text', Object.assign({ const labelNode = svg.make('text', Object.assign({
'y': this.currentY + config.padding.top + sz, 'y': this.currentY + config.padding.top + sz,
'text-anchor': anchor, 'text-anchor': anchor,
}, config.labelAttrs)); }, config.labelAttrs));
labelNode.appendChild(makeText(label)); labelNode.appendChild(svg.makeText(label));
this.actions.appendChild(labelNode); this.actions.appendChild(labelNode);
const w = labelNode.getComputedTextLength(); const w = labelNode.getComputedTextLength();
@ -847,54 +815,56 @@ define(() => {
} }
renderBlockBegin(scope) { renderBlockBegin(scope) {
this.currentY += BLOCK_MARGIN.top; this.currentY += BLOCK.margin.top;
scope.y = this.currentY; scope.y = this.currentY;
scope.first = true; scope.first = true;
} }
renderSectionBegin(scope, {left, right}, {mode, label}) { renderSectionBegin(scope, {left, right}, {mode, label}) {
/* jshint -W071 */ // TODO: tidy this up (split text rendering)
const agentInfoL = this.agentInfos.get(left); const agentInfoL = this.agentInfos.get(left);
const agentInfoR = this.agentInfos.get(right); const agentInfoR = this.agentInfos.get(right);
if(scope.first) { if(scope.first) {
scope.first = false; scope.first = false;
} else { } else {
this.currentY += BLOCK_SECTION_PADDING.bottom; this.currentY += BLOCK.section.padding.bottom;
this.sections.appendChild(makeSVGNode('path', Object.assign({ this.sections.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M' + agentInfoL.x + ' ' + this.currentY + 'M' + agentInfoL.x + ' ' + this.currentY +
' L' + agentInfoR.x + ' ' + this.currentY ' L' + agentInfoR.x + ' ' + this.currentY
), ),
}, ATTRS.BLOCK_SEPARATOR))); }, BLOCK.separator.attrs)));
} }
let x = agentInfoL.x; let x = agentInfoL.x;
if(mode) { if(mode) {
const sz = ATTRS.BLOCK_MODE_LABEL['font-size']; const sz = BLOCK.section.mode.labelAttrs['font-size'];
const modeBox = makeSVGNode('rect', Object.assign({ const modeBox = svg.make('rect', Object.assign({
'x': x, 'x': x,
'y': this.currentY, 'y': this.currentY,
'height': ( 'height': (
sz * LINE_HEIGHT + sz * LINE_HEIGHT +
BLOCK_MODE_PADDING.top + BLOCK.section.mode.padding.top +
BLOCK_MODE_PADDING.bottom BLOCK.section.mode.padding.bottom
), ),
}, ATTRS.BLOCK_MODE)); }, BLOCK.section.mode.boxAttrs));
const modeLabel = makeSVGNode('text', Object.assign({ const modeLabel = svg.make('text', Object.assign({
'x': x + BLOCK_MODE_PADDING.left, 'x': x + BLOCK.section.mode.padding.left,
'y': ( 'y': (
this.currentY + sz + this.currentY + sz +
BLOCK_MODE_PADDING.top BLOCK.section.mode.padding.top
), ),
}, ATTRS.BLOCK_MODE_LABEL)); }, BLOCK.section.mode.labelAttrs));
modeLabel.appendChild(makeText(mode)); modeLabel.appendChild(svg.makeText(mode));
this.blocks.appendChild(modeBox); this.blocks.appendChild(modeBox);
this.actions.appendChild(modeLabel); this.actions.appendChild(modeLabel);
const w = ( const w = (
modeLabel.getComputedTextLength() + modeLabel.getComputedTextLength() +
BLOCK_MODE_PADDING.left + BLOCK.section.mode.padding.left +
BLOCK_MODE_PADDING.right BLOCK.section.mode.padding.right
); );
modeBox.setAttribute('width', w); modeBox.setAttribute('width', w);
x += w; x += w;
@ -903,47 +873,47 @@ define(() => {
} }
if(label) { if(label) {
x += BLOCK_LABEL_PADDING.left; x += BLOCK.section.label.labelPadding.left;
const sz = ATTRS.BLOCK_LABEL['font-size']; const sz = BLOCK.section.label.labelAttrs['font-size'];
const mask = makeSVGNode('rect', Object.assign({ const mask = svg.make('rect', Object.assign({
'x': x - BLOCK_LABEL_MASK_PADDING.left, 'x': x - BLOCK.section.label.maskPadding.left,
'y': this.currentY - sz * LINE_HEIGHT, 'y': this.currentY - sz * LINE_HEIGHT,
'height': sz * LINE_HEIGHT, 'height': sz * LINE_HEIGHT,
}, ATTRS.BLOCK_LABEL_MASK)); }, BLOCK.section.label.maskAttrs));
const labelLabel = makeSVGNode('text', Object.assign({ const labelLabel = svg.make('text', Object.assign({
'x': x, 'x': x,
'y': this.currentY - sz * (LINE_HEIGHT - 1), 'y': this.currentY - sz * (LINE_HEIGHT - 1),
}, ATTRS.BLOCK_LABEL)); }, BLOCK.section.label.labelAttrs));
labelLabel.appendChild(makeText(label)); labelLabel.appendChild(svg.makeText(label));
this.actions.appendChild(mask); this.actions.appendChild(mask);
this.actions.appendChild(labelLabel); this.actions.appendChild(labelLabel);
const w = ( const w = (
labelLabel.getComputedTextLength() + labelLabel.getComputedTextLength() +
BLOCK_LABEL_MASK_PADDING.left + BLOCK.section.label.maskPadding.left +
BLOCK_LABEL_MASK_PADDING.right BLOCK.section.label.maskPadding.right
); );
mask.setAttribute('width', w); mask.setAttribute('width', w);
} }
this.currentY += BLOCK_SECTION_PADDING.top; this.currentY += BLOCK.section.padding.top;
} }
renderSectionEnd(/*scope, block, section*/) { renderSectionEnd(/*scope, block, section*/) {
} }
renderBlockEnd(scope, {left, right}) { renderBlockEnd(scope, {left, right}) {
this.currentY += BLOCK_SECTION_PADDING.bottom; this.currentY += BLOCK.section.padding.bottom;
const agentInfoL = this.agentInfos.get(left); const agentInfoL = this.agentInfos.get(left);
const agentInfoR = this.agentInfos.get(right); const agentInfoR = this.agentInfos.get(right);
this.blocks.appendChild(makeSVGNode('rect', Object.assign({ this.blocks.appendChild(svg.make('rect', Object.assign({
'x': agentInfoL.x, 'x': agentInfoL.x,
'y': scope.y, 'y': scope.y,
'width': agentInfoR.x - agentInfoL.x, 'width': agentInfoR.x - agentInfoL.x,
'height': this.currentY - scope.y, 'height': this.currentY - scope.y,
}, ATTRS.BLOCK_BOX))); }, BLOCK.boxAttrs)));
this.currentY += BLOCK_MARGIN.bottom + ACTION_MARGIN; this.currentY += BLOCK.margin.bottom + ACTION_MARGIN;
} }
addAction(stage) { addAction(stage) {
@ -953,8 +923,8 @@ define(() => {
testTextWidth(attrs, content) { testTextWidth(attrs, content) {
let tester = this.testersCache.get(attrs); let tester = this.testersCache.get(attrs);
if(!tester) { if(!tester) {
const text = makeText(); const text = svg.makeText();
const node = makeSVGNode('text', attrs); const node = svg.make('text', attrs);
node.appendChild(text); node.appendChild(text);
this.testers.appendChild(node); this.testers.appendChild(node);
tester = {text, node}; tester = {text, node};
@ -966,7 +936,7 @@ define(() => {
} }
buildAgentInfos(agents, stages) { buildAgentInfos(agents, stages) {
empty(this.testers); svg.empty(this.testers);
this.testersCache.clear(); this.testersCache.clear();
this.diagram.appendChild(this.testers); this.diagram.appendChild(this.testers);
@ -976,7 +946,7 @@ define(() => {
label: agent, label: agent,
labelWidth: ( labelWidth: (
this.testTextWidth(ATTRS.AGENT_BOX_LABEL, agent) + this.testTextWidth(ATTRS.AGENT_BOX_LABEL, agent) +
BOX_PADDING * 2 AGENT_BOX_PADDING * 2
), ),
index, index,
x: null, x: null,
@ -1036,11 +1006,11 @@ define(() => {
} }
render({meta, agents, stages}) { render({meta, agents, stages}) {
empty(this.agentLines); svg.empty(this.agentLines);
empty(this.blocks); svg.empty(this.blocks);
empty(this.sections); svg.empty(this.sections);
empty(this.agentDecor); svg.empty(this.agentDecor);
empty(this.actions); svg.empty(this.actions);
this.titleText.nodeValue = meta.title || ''; this.titleText.nodeValue = meta.title || '';

View File

@ -0,0 +1,39 @@
define(() => {
'use strict';
const NS = 'http://www.w3.org/2000/svg';
function makeText(text = '') {
return document.createTextNode(text);
}
function make(type, attrs = {}) {
const o = document.createElementNS(NS, type);
for(let k in attrs) {
if(attrs.hasOwnProperty(k)) {
o.setAttribute(k, attrs[k]);
}
}
return o;
}
function makeContainer(attrs = {}) {
return make('svg', Object.assign({
'xmlns': NS,
'version': '1.1',
}, attrs));
}
function empty(node) {
while(node.childNodes.length > 0) {
node.removeChild(node.lastChild);
}
}
return {
makeText,
make,
makeContainer,
empty,
};
});

View File

@ -0,0 +1,58 @@
defineDescribe('SVGUtilities', ['./SVGUtilities'], (svg) => {
'use strict';
const expectedNS = 'http://www.w3.org/2000/svg';
describe('.makeText', () => {
it('creates a text node with the given content', () => {
const node = svg.makeText('foo');
expect(node.nodeValue).toEqual('foo');
});
it('defaults to empty', () => {
const node = svg.makeText();
expect(node.nodeValue).toEqual('');
});
});
describe('.make', () => {
it('creates a node with the SVG namespace', () => {
const node = svg.make('path');
expect(node.namespaceURI).toEqual(expectedNS);
expect(node.tagName).toEqual('path');
});
it('assigns the given attributes', () => {
const node = svg.make('path', {'foo': 'bar'});
expect(node.getAttribute('foo')).toEqual('bar');
});
});
describe('.makeContainer', () => {
it('creates an svg node with the SVG namespace', () => {
const node = svg.makeContainer();
expect(node.namespaceURI).toEqual(expectedNS);
expect(node.getAttribute('xmlns')).toEqual(expectedNS);
expect(node.getAttribute('version')).toEqual('1.1');
expect(node.tagName).toEqual('svg');
});
it('assigns the given attributes', () => {
const node = svg.makeContainer({'foo': 'bar'});
expect(node.getAttribute('foo')).toEqual('bar');
});
});
describe('.empty', () => {
it('removes all child nodes from the given node', () => {
const node = document.createElement('p');
const a = document.createElement('p');
const b = document.createElement('p');
node.appendChild(a);
node.appendChild(b);
svg.empty(node);
expect(node.children.length).toEqual(0);
});
});
});

View File

@ -3,4 +3,6 @@ define([
'sequence/Parser_spec', 'sequence/Parser_spec',
'sequence/Generator_spec', 'sequence/Generator_spec',
'sequence/Renderer_spec', 'sequence/Renderer_spec',
'sequence/ArrayUtilities_spec',
'sequence/SVGUtilities_spec',
]); ]);