Begin separating theme out of renderer

This commit is contained in:
David Evans 2017-10-28 12:53:41 +01:00
parent 1aac54cefc
commit b0ba84b4eb
15 changed files with 500 additions and 402 deletions

View File

@ -3,16 +3,19 @@
requirejs.config(window.getRequirejsCDN()); requirejs.config(window.getRequirejsCDN());
/* jshint -W072 */
requirejs([ requirejs([
'interface/Interface', 'interface/Interface',
'sequence/Parser', 'sequence/Parser',
'sequence/Generator', 'sequence/Generator',
'sequence/Renderer', 'sequence/Renderer',
'sequence/themes/Basic',
], ( ], (
Interface, Interface,
Parser, Parser,
Generator, Generator,
Renderer Renderer,
Theme
) => { ) => {
const defaultCode = ( const defaultCode = (
'title Labyrinth\n' + 'title Labyrinth\n' +
@ -36,7 +39,7 @@
defaultCode, defaultCode,
parser: new Parser(), parser: new Parser(),
generator: new Generator(), generator: new Generator(),
renderer: new Renderer(), renderer: new Renderer(new Theme()),
}); });
ui.build(document.body); ui.build(document.body);
}); });

View File

@ -1,4 +1,4 @@
define(['./ArrayUtilities'], (array) => { define(['core/ArrayUtilities'], (array) => {
'use strict'; 'use strict';
class AgentState { class AgentState {

View File

@ -1,8 +1,8 @@
define([ define([
'./ArrayUtilities', 'core/ArrayUtilities',
'./SVGUtilities', 'svg/SVGUtilities',
'./SVGTextBlock', 'svg/SVGTextBlock',
'./SVGShapes', 'svg/SVGShapes',
], ( ], (
array, array,
svg, svg,
@ -13,219 +13,6 @@ define([
const SEP_ZERO = {left: 0, right: 0}; const SEP_ZERO = {left: 0, right: 0};
const LINE_HEIGHT = 1.3;
const TITLE_MARGIN = 10;
const OUTER_MARGIN = 5;
const AGENT_MARGIN = 10;
const ACTION_MARGIN = 5;
const AGENT_CAP = {
box: {
padding: {
top: 5,
left: 10,
right: 10,
bottom: 5,
},
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
},
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
},
bar: {
attrs: {
'fill': '#000000',
'height': 5,
},
},
none: {
height: 10,
},
};
const CONNECT = {
lineAttrs: {
'solid': {
'fill': 'none',
'stroke': '#000000',
'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,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
mask: {
padding: {
top: 0,
left: 3,
right: 3,
bottom: 0,
},
maskAttrs: {
'fill': '#FFFFFF',
},
},
};
const BLOCK = {
margin: {
top: 0,
bottom: 0,
},
boxAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
},
section: {
padding: {
top: 3,
bottom: 2,
},
mode: {
padding: {
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,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
label: {
padding: {
top: 1,
left: 5,
right: 3,
bottom: 0,
},
maskAttrs: {
'fill': '#FFFFFF',
},
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
},
separator: {
attrs: {
'stroke': '#000000',
'stroke-width': 1.5,
'stroke-dasharray': '4, 2',
},
},
};
const NOTE = {
'note': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 5, left: 5, right: 10, bottom: 5},
overlap: {left: 10, right: 10},
boxRenderer: SVGShapes.renderNote.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
}, {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
'state': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 7, left: 7, right: 7, bottom: 7},
overlap: {left: 10, right: 10},
boxRenderer: SVGShapes.renderBox.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 10,
'ry': 10,
}),
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
};
const ATTRS = {
TITLE: {
'font-family': 'sans-serif',
'font-size': 20,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
'class': 'title',
},
AGENT_LINE: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
};
function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) { function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) {
container.appendChild(svg.make( container.appendChild(svg.make(
attrs.fill === 'none' ? 'polyline' : 'polygon', attrs.fill === 'none' ? 'polyline' : 'polygon',
@ -265,7 +52,7 @@ define([
} }
return class Renderer { return class Renderer {
constructor() { constructor(theme) {
this.separationAgentCap = { this.separationAgentCap = {
'box': this.separationAgentCapBox.bind(this), 'box': this.separationAgentCapBox.bind(this),
'cross': this.separationAgentCapCross.bind(this), 'cross': this.separationAgentCapCross.bind(this),
@ -317,6 +104,8 @@ define([
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.theme = theme;
this.currentSequence = null;
this.buildStaticElements(); this.buildStaticElements();
} }
@ -338,7 +127,7 @@ define([
this.base.appendChild(this.sections); this.base.appendChild(this.sections);
this.base.appendChild(this.actionShapes); this.base.appendChild(this.actionShapes);
this.base.appendChild(this.actionLabels); this.base.appendChild(this.actionLabels);
this.title = new SVGTextBlock(this.base, ATTRS.TITLE); this.title = new SVGTextBlock(this.base);
this.sizer = new SVGTextBlock.SizeTester(this.base); this.sizer = new SVGTextBlock.SizeTester(this.base);
} }
@ -385,17 +174,18 @@ define([
this.addSeparation( this.addSeparation(
agentR, agentR,
agentL, agentL,
sepR.left + sepL.right + AGENT_MARGIN sepR.left + sepL.right + this.theme.agentMargin
); );
}); });
}); });
} }
separationAgentCapBox({label}) { separationAgentCapBox({label}) {
const config = this.theme.agentCap.box;
const width = ( const width = (
this.sizer.measure(AGENT_CAP.box.labelAttrs, label).width + this.sizer.measure(config.labelAttrs, label).width +
AGENT_CAP.box.padding.left + config.padding.left +
AGENT_CAP.box.padding.right config.padding.right
); );
return { return {
@ -405,17 +195,19 @@ define([
} }
separationAgentCapCross() { separationAgentCapCross() {
const config = this.theme.agentCap.cross;
return { return {
left: AGENT_CAP.cross.size / 2, left: config.size / 2,
right: AGENT_CAP.cross.size / 2, right: config.size / 2,
}; };
} }
separationAgentCapBar({label}) { separationAgentCapBar({label}) {
const config = this.theme.agentCap.box;
const width = ( const width = (
this.sizer.measure(AGENT_CAP.box.labelAttrs, label).width + this.sizer.measure(config.labelAttrs, label).width +
AGENT_CAP.box.padding.left + config.padding.left +
AGENT_CAP.box.padding.right config.padding.right
); );
return { return {
@ -447,19 +239,21 @@ define([
} }
separationConnection({agents, label}) { separationConnection({agents, label}) {
const config = this.theme.connect;
this.addSeparation( this.addSeparation(
agents[0], agents[0],
agents[1], agents[1],
this.sizer.measure(CONNECT.label.attrs, label).width + this.sizer.measure(config.label.attrs, label).width +
CONNECT.arrow.width * 2 + config.arrow.width * 2 +
CONNECT.label.padding * 2 + config.label.padding * 2 +
ATTRS.AGENT_LINE['stroke-width'] this.theme.agentLineAttrs['stroke-width']
); );
} }
separationNoteOver({agents, mode, label}) { separationNoteOver({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
const width = ( const width = (
this.sizer.measure(config.labelAttrs, label).width + this.sizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
@ -491,7 +285,7 @@ define([
} }
separationNoteLeft({agents, mode, label}) { separationNoteLeft({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
const {left} = this.findExtremes(agents); const {left} = this.findExtremes(agents);
const agentSpaces = new Map(); const agentSpaces = new Map();
@ -509,7 +303,7 @@ define([
} }
separationNoteRight({agents, mode, label}) { separationNoteRight({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
const {right} = this.findExtremes(agents); const {right} = this.findExtremes(agents);
const agentSpaces = new Map(); const agentSpaces = new Map();
@ -527,7 +321,7 @@ define([
} }
separationNoteBetween({agents, mode, label}) { separationNoteBetween({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agents);
this.addSeparation( this.addSeparation(
@ -548,7 +342,7 @@ define([
} }
separationSectionBegin(scope, {left, right}, {mode, label}) { separationSectionBegin(scope, {left, right}, {mode, label}) {
const config = BLOCK.section; const config = this.theme.block.section;
const width = ( const width = (
this.sizer.measure(config.mode.labelAttrs, mode).width + this.sizer.measure(config.mode.labelAttrs, mode).width +
config.mode.padding.left + config.mode.padding.left +
@ -569,12 +363,13 @@ define([
} }
renderAgentCapBox({x, label}) { renderAgentCapBox({x, label}) {
const config = this.theme.agentCap.box;
const {height} = SVGShapes.renderBoxedText(label, { const {height} = SVGShapes.renderBoxedText(label, {
x, x,
y: this.currentY, y: this.currentY,
padding: AGENT_CAP.box.padding, padding: config.padding,
boxAttrs: AGENT_CAP.box.boxAttrs, boxAttrs: config.boxAttrs,
labelAttrs: AGENT_CAP.box.labelAttrs, labelAttrs: config.labelAttrs,
boxLayer: this.actionShapes, boxLayer: this.actionShapes,
labelLayer: this.actionLabels, labelLayer: this.actionLabels,
}); });
@ -587,8 +382,9 @@ define([
} }
renderAgentCapCross({x}) { renderAgentCapCross({x}) {
const config = this.theme.agentCap.cross;
const y = this.currentY; const y = this.currentY;
const d = AGENT_CAP.cross.size / 2; const d = config.size / 2;
this.actionShapes.appendChild(svg.make('path', Object.assign({ this.actionShapes.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
@ -597,7 +393,7 @@ define([
' M ' + (x + d) + ' ' + y + ' M ' + (x + d) + ' ' + y +
' L ' + (x - d) + ' ' + (y + d * 2) ' L ' + (x - d) + ' ' + (y + d * 2)
), ),
}, AGENT_CAP.cross.attrs))); }, config.attrs)));
return { return {
lineTop: d, lineTop: d,
@ -607,30 +403,33 @@ define([
} }
renderAgentCapBar({x, label}) { renderAgentCapBar({x, label}) {
const configB = this.theme.agentCap.box;
const config = this.theme.agentCap.bar;
const width = ( const width = (
this.sizer.measure(AGENT_CAP.box.labelAttrs, label).width + this.sizer.measure(configB.labelAttrs, label).width +
AGENT_CAP.box.padding.left + configB.padding.left +
AGENT_CAP.box.padding.right configB.padding.right
); );
this.actionShapes.appendChild(svg.make('rect', Object.assign({ this.actionShapes.appendChild(svg.make('rect', Object.assign({
'x': x - width / 2, 'x': x - width / 2,
'y': this.currentY, 'y': this.currentY,
'width': width, 'width': width,
}, AGENT_CAP.bar.attrs))); }, config.attrs)));
return { return {
lineTop: 0, lineTop: 0,
lineBottom: AGENT_CAP.bar.attrs.height, lineBottom: config.attrs.height,
height: AGENT_CAP.bar.attrs.height, height: config.attrs.height,
}; };
} }
renderAgentCapNone() { renderAgentCapNone() {
const config = this.theme.agentCap.none;
return { return {
lineTop: AGENT_CAP.none.height, lineTop: config.height,
lineBottom: 0, lineBottom: 0,
height: AGENT_CAP.none.height, height: config.height,
}; };
} }
@ -642,7 +441,7 @@ define([
maxHeight = Math.max(maxHeight, shifts.height); maxHeight = Math.max(maxHeight, shifts.height);
agentInfo.latestYStart = this.currentY + shifts.lineBottom; agentInfo.latestYStart = this.currentY + shifts.lineBottom;
}); });
this.currentY += maxHeight + ACTION_MARGIN; this.currentY += maxHeight + this.theme.actionMargin;
} }
renderAgentEnd({mode, agents}) { renderAgentEnd({mode, agents}) {
@ -658,34 +457,35 @@ define([
'x2': x, 'x2': x,
'y2': this.currentY + shifts.lineTop, 'y2': this.currentY + shifts.lineTop,
'class': 'agent-' + agentInfo.index + '-line', 'class': 'agent-' + agentInfo.index + '-line',
}, ATTRS.AGENT_LINE))); }, this.theme.agentLineAttrs)));
agentInfo.latestYStart = null; agentInfo.latestYStart = null;
}); });
this.currentY += maxHeight + ACTION_MARGIN; this.currentY += maxHeight + this.theme.actionMargin;
} }
renderConnection({label, agents, line, left, right}) { renderConnection({label, agents, line, left, right}) {
const config = this.theme.connect;
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.arrow.height / 2; const dy = config.arrow.height / 2;
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 = this.theme.agentLineAttrs['stroke-width'];
const height = ( const height = (
this.sizer.measureHeight(CONNECT.label.attrs, label) + this.sizer.measureHeight(config.label.attrs, label) +
CONNECT.label.margin.top + config.label.margin.top +
CONNECT.label.margin.bottom config.label.margin.bottom
); );
let y = this.currentY + Math.max(dy, height); let y = this.currentY + Math.max(dy, height);
SVGShapes.renderBoxedText(label, { SVGShapes.renderBoxedText(label, {
x: (from.x + to.x) / 2, x: (from.x + to.x) / 2,
y: y - height + CONNECT.label.margin.top, y: y - height + config.label.margin.top,
padding: CONNECT.mask.padding, padding: config.mask.padding,
boxAttrs: CONNECT.mask.maskAttrs, boxAttrs: config.mask.maskAttrs,
labelAttrs: CONNECT.label.attrs, labelAttrs: config.label.attrs,
boxLayer: this.mask, boxLayer: this.mask,
labelLayer: this.actionLabels, labelLayer: this.actionLabels,
}); });
@ -695,15 +495,15 @@ define([
'y1': y, 'y1': y,
'x2': to.x - (right ? short : 0) * dir, 'x2': to.x - (right ? short : 0) * dir,
'y2': y, 'y2': y,
}, CONNECT.lineAttrs[line]))); }, config.lineAttrs[line])));
if(left) { if(left) {
drawHorizontalArrowHead(this.actionShapes, { drawHorizontalArrowHead(this.actionShapes, {
x: from.x + short * dir, x: from.x + short * dir,
y, y,
dx: CONNECT.arrow.width * dir, dx: config.arrow.width * dir,
dy, dy,
attrs: CONNECT.arrow.attrs, attrs: config.arrow.attrs,
}); });
} }
@ -711,26 +511,26 @@ define([
drawHorizontalArrowHead(this.actionShapes, { drawHorizontalArrowHead(this.actionShapes, {
x: to.x - short * dir, x: to.x - short * dir,
y, y,
dx: -CONNECT.arrow.width * dir, dx: -config.arrow.width * dir,
dy, dy,
attrs: CONNECT.arrow.attrs, attrs: config.arrow.attrs,
}); });
} }
this.currentY = y + dy + ACTION_MARGIN; this.currentY = y + dy + this.theme.actionMargin;
} }
renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) { renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) {
const config = NOTE[mode]; const config = this.theme.note[mode];
this.currentY += config.margin.top; this.currentY += config.margin.top;
const y = this.currentY + config.padding.top; const y = this.currentY + config.padding.top;
const labelNode = new SVGTextBlock( const labelNode = new SVGTextBlock(this.actionLabels, {
this.actionLabels, attrs: config.labelAttrs,
config.labelAttrs, text: label,
{text: label, y} y,
); });
const fullW = ( const fullW = (
labelNode.width + labelNode.width +
@ -752,16 +552,19 @@ define([
} }
switch(config.labelAttrs['text-anchor']) { switch(config.labelAttrs['text-anchor']) {
case 'middle': case 'middle':
labelNode.reanchor(( labelNode.set({
x: (
x0 + config.padding.left + x0 + config.padding.left +
x1 - config.padding.right x1 - config.padding.right
) / 2, y); ) / 2,
y,
});
break; break;
case 'end': case 'end':
labelNode.reanchor(x1 - config.padding.right, y); labelNode.set({x: x1 - config.padding.right, y});
break; break;
default: default:
labelNode.reanchor(x0 + config.padding.left, y); labelNode.set({x: x0 + config.padding.left, y});
break; break;
} }
@ -775,12 +578,12 @@ define([
this.currentY += ( this.currentY += (
fullH + fullH +
config.margin.bottom + config.margin.bottom +
ACTION_MARGIN this.theme.actionMargin
); );
} }
renderNoteOver({agents, mode, label}) { renderNoteOver({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
if(agents.length > 1) { if(agents.length > 1) {
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agents);
@ -795,7 +598,7 @@ define([
} }
renderNoteLeft({agents, mode, label}) { renderNoteLeft({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
const {left} = this.findExtremes(agents); const {left} = this.findExtremes(agents);
const x1 = this.agentInfos.get(left).x - config.margin.right; const x1 = this.agentInfos.get(left).x - config.margin.right;
@ -803,7 +606,7 @@ define([
} }
renderNoteRight({agents, mode, label}) { renderNoteRight({agents, mode, label}) {
const config = NOTE[mode]; const config = this.theme.note[mode];
const {right} = this.findExtremes(agents); const {right} = this.findExtremes(agents);
const x0 = this.agentInfos.get(right).x + config.margin.left; const x0 = this.agentInfos.get(right).x + config.margin.left;
@ -821,34 +624,35 @@ define([
} }
renderBlockBegin(scope) { renderBlockBegin(scope) {
this.currentY += BLOCK.margin.top; this.currentY += this.theme.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}) {
const config = this.theme.block;
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 += config.section.padding.bottom;
this.sections.appendChild(svg.make('line', Object.assign({ this.sections.appendChild(svg.make('line', Object.assign({
'x1': agentInfoL.x, 'x1': agentInfoL.x,
'y1': this.currentY, 'y1': this.currentY,
'x2': agentInfoR.x, 'x2': agentInfoR.x,
'y2': this.currentY, 'y2': this.currentY,
}, BLOCK.separator.attrs))); }, config.separator.attrs)));
} }
const modeRender = SVGShapes.renderBoxedText(mode, { const modeRender = SVGShapes.renderBoxedText(mode, {
x: agentInfoL.x, x: agentInfoL.x,
y: this.currentY, y: this.currentY,
padding: BLOCK.section.mode.padding, padding: config.section.mode.padding,
boxAttrs: BLOCK.section.mode.boxAttrs, boxAttrs: config.section.mode.boxAttrs,
labelAttrs: BLOCK.section.mode.labelAttrs, labelAttrs: config.section.mode.labelAttrs,
boxLayer: this.blocks, boxLayer: this.blocks,
labelLayer: this.actionLabels, labelLayer: this.actionLabels,
}); });
@ -856,16 +660,16 @@ define([
const labelRender = SVGShapes.renderBoxedText(label, { const labelRender = SVGShapes.renderBoxedText(label, {
x: agentInfoL.x + modeRender.width, x: agentInfoL.x + modeRender.width,
y: this.currentY, y: this.currentY,
padding: BLOCK.section.label.padding, padding: config.section.label.padding,
boxAttrs: BLOCK.section.label.maskAttrs, boxAttrs: config.section.label.maskAttrs,
labelAttrs: BLOCK.section.label.labelAttrs, labelAttrs: config.section.label.labelAttrs,
boxLayer: this.mask, boxLayer: this.mask,
labelLayer: this.actionLabels, labelLayer: this.actionLabels,
}); });
this.currentY += ( this.currentY += (
Math.max(modeRender.height, labelRender.height) + Math.max(modeRender.height, labelRender.height) +
BLOCK.section.padding.top config.section.padding.top
); );
} }
@ -873,7 +677,8 @@ define([
} }
renderBlockEnd(scope, {left, right}) { renderBlockEnd(scope, {left, right}) {
this.currentY += BLOCK.section.padding.bottom; const config = this.theme.block;
this.currentY += config.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);
@ -882,9 +687,9 @@ define([
'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,
}, BLOCK.boxAttrs))); }, config.boxAttrs)));
this.currentY += BLOCK.margin.bottom + ACTION_MARGIN; this.currentY += config.margin.bottom + this.theme.actionMargin;
} }
addAction(stage) { addAction(stage) {
@ -924,15 +729,16 @@ define([
updateBounds(stagesHeight) { updateBounds(stagesHeight) {
const cx = (this.minX + this.maxX) / 2; const cx = (this.minX + this.maxX) / 2;
const titleY = ((this.title.height > 0) ? const titleY = ((this.title.height > 0) ?
(-TITLE_MARGIN - this.title.height) : 0 (-this.theme.titleMargin - this.title.height) : 0
); );
this.title.reanchor(cx, titleY); this.title.set({x: cx, y: titleY});
const halfTitleWidth = this.title.width / 2; const halfTitleWidth = this.title.width / 2;
const x0 = Math.min(this.minX, cx - halfTitleWidth) - OUTER_MARGIN; const margin = this.theme.outerMargin;
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + OUTER_MARGIN; const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
const y0 = titleY - OUTER_MARGIN; const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
const y1 = stagesHeight + OUTER_MARGIN; const y0 = titleY - margin;
const y1 = stagesHeight + margin;
this.base.setAttribute('viewBox', ( this.base.setAttribute('viewBox', (
x0 + ' ' + y0 + ' ' + x0 + ' ' + y0 + ' ' +
@ -942,7 +748,17 @@ define([
this.height = (y1 - y0); this.height = (y1 - y0);
} }
render({meta, agents, stages}) { setTheme(theme) {
if(this.theme === theme) {
return;
}
this.theme = theme;
if(this.currentSequence) {
this.render(this.currentSequence);
}
}
render(sequence) {
svg.empty(this.agentLines); svg.empty(this.agentLines);
svg.empty(this.mask); svg.empty(this.mask);
svg.empty(this.blocks); svg.empty(this.blocks);
@ -950,20 +766,27 @@ define([
svg.empty(this.actionShapes); svg.empty(this.actionShapes);
svg.empty(this.actionLabels); svg.empty(this.actionLabels);
this.title.setText(meta.title); this.title.set({
attrs: this.theme.titleAttrs,
text: sequence.meta.title,
});
this.minX = 0; this.minX = 0;
this.maxX = 0; this.maxX = 0;
this.buildAgentInfos(agents, stages); this.buildAgentInfos(sequence.agents, sequence.stages);
this.currentY = 0; this.currentY = 0;
traverse(stages, this.renderTraversalFns); traverse(sequence.stages, this.renderTraversalFns);
const stagesHeight = Math.max(this.currentY - ACTION_MARGIN, 0); const stagesHeight = Math.max(
this.currentY - this.theme.actionMargin,
0
);
this.updateBounds(stagesHeight); this.updateBounds(stagesHeight);
this.sizer.resetCache(); this.sizer.resetCache();
this.sizer.detach(); this.sizer.detach();
this.currentSequence = sequence;
} }
getAgentX(name) { getAgentX(name) {

View File

@ -1,10 +1,16 @@
defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => { defineDescribe('Sequence Renderer', [
'./Renderer',
'./themes/Basic',
], (
Renderer,
Theme
) => {
'use strict'; 'use strict';
let renderer = null; let renderer = null;
beforeEach(() => { beforeEach(() => {
renderer = new Renderer(); renderer = new Renderer(new Theme());
document.body.appendChild(renderer.svg()); document.body.appendChild(renderer.svg());
}); });

View File

@ -0,0 +1,233 @@
define([
'core/ArrayUtilities',
'svg/SVGUtilities',
'svg/SVGTextBlock',
'svg/SVGShapes',
], (
array,
svg,
SVGTextBlock,
SVGShapes
) => {
'use strict';
const LINE_HEIGHT = 1.3;
const SETTINGS = {
titleMargin: 10,
outerMargin: 5,
agentMargin: 10,
actionMargin: 5,
agentCap: {
box: {
padding: {
top: 5,
left: 10,
right: 10,
bottom: 5,
},
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
},
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
},
bar: {
attrs: {
'fill': '#000000',
'height': 5,
},
},
none: {
height: 10,
},
},
connect: {
lineAttrs: {
'solid': {
'fill': 'none',
'stroke': '#000000',
'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,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
mask: {
padding: {
top: 0,
left: 3,
right: 3,
bottom: 0,
},
maskAttrs: {
'fill': '#FFFFFF',
},
},
},
block: {
margin: {
top: 0,
bottom: 0,
},
boxAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
},
section: {
padding: {
top: 3,
bottom: 2,
},
mode: {
padding: {
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,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
label: {
padding: {
top: 1,
left: 5,
right: 3,
bottom: 0,
},
maskAttrs: {
'fill': '#FFFFFF',
},
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
},
separator: {
attrs: {
'stroke': '#000000',
'stroke-width': 1.5,
'stroke-dasharray': '4, 2',
},
},
},
note: {
'note': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 5, left: 5, right: 10, bottom: 5},
overlap: {left: 10, right: 10},
boxRenderer: SVGShapes.renderNote.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
}, {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
'state': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 7, left: 7, right: 7, bottom: 7},
overlap: {left: 10, right: 10},
boxRenderer: SVGShapes.renderBox.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 10,
'ry': 10,
}),
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
},
titleAttrs: {
'font-family': 'sans-serif',
'font-size': 20,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
'class': 'title',
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
};
return class Theme {
constructor() {
Object.assign(this, SETTINGS);
}
};
});

View File

@ -0,0 +1,8 @@
defineDescribe('Basic Theme', ['./Basic'], (Theme) => {
'use strict';
it('contains settings for the theme', () => {
const theme = new Theme();
expect(theme.outerMargin).toEqual(5);
});
});

View File

@ -1,10 +1,11 @@
define([ define([
'core/ArrayUtilities_spec',
'svg/SVGUtilities_spec',
'svg/SVGTextBlock_spec',
'svg/SVGShapes_spec',
'interface/Interface_spec', 'interface/Interface_spec',
'sequence/Parser_spec', 'sequence/Parser_spec',
'sequence/Generator_spec', 'sequence/Generator_spec',
'sequence/Renderer_spec', 'sequence/Renderer_spec',
'sequence/ArrayUtilities_spec', 'sequence/themes/Basic_spec',
'sequence/SVGUtilities_spec',
'sequence/SVGTextBlock_spec',
'sequence/SVGShapes_spec',
]); ]);

View File

@ -65,7 +65,8 @@ define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => {
break; break;
} }
const label = new SVGTextBlock(labelLayer, labelAttrs, { const label = new SVGTextBlock(labelLayer, {
attrs: labelAttrs,
text, text,
x: anchorX, x: anchorX,
y: y + padding.top, y: y + padding.top,

View File

@ -65,9 +65,9 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
boxLayer: o, boxLayer: o,
labelLayer: o, labelLayer: o,
}); });
expect(rendered.label.text).toEqual('foo'); expect(rendered.label.state.text).toEqual('foo');
expect(rendered.label.x).toEqual(5); expect(rendered.label.state.x).toEqual(5);
expect(rendered.label.y).toEqual(10); expect(rendered.label.state.y).toEqual(10);
expect(rendered.label.firstLine().parentNode).toEqual(o); expect(rendered.label.firstLine().parentNode).toEqual(o);
}); });

View File

@ -10,37 +10,37 @@ define(['./SVGUtilities'], (svg) => {
}; };
} }
function merge(state, newState) {
for(let k in state) {
if(state.hasOwnProperty(k)) {
if(newState[k] !== null && newState[k] !== undefined) {
state[k] = newState[k];
}
}
}
}
class SVGTextBlock { class SVGTextBlock {
constructor( constructor(container, initialState = {}) {
container,
attrs,
{text = '', x = 0, y = 0} = {}
) {
this.container = container; this.container = container;
this.attrs = attrs; this.state = {
this.text = ''; attrs: {},
this.x = x; text: '',
this.y = y; x: 0,
y: 0,
};
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.nodes = []; this.nodes = [];
this.setText(text); this.set(initialState);
}
_updateY() {
const {size, lineHeight} = fontDetails(this.attrs);
this.nodes.forEach(({element}, i) => {
element.setAttribute('y', this.y + i * lineHeight + size);
});
this.height = lineHeight * this.nodes.length;
} }
_rebuildNodes(count) { _rebuildNodes(count) {
if(count === this.nodes.length) {
return;
}
if(count > this.nodes.length) { if(count > this.nodes.length) {
const attrs = Object.assign({'x': this.x}, this.attrs); const attrs = Object.assign({
'x': this.state.x,
}, this.state.attrs);
while(this.nodes.length < count) { while(this.nodes.length < count) {
const element = svg.make('text', attrs); const element = svg.make('text', attrs);
const text = svg.makeText(); const text = svg.makeText();
@ -54,29 +54,23 @@ define(['./SVGUtilities'], (svg) => {
this.container.removeChild(element); this.container.removeChild(element);
} }
} }
this._updateY();
} }
firstLine() { _reset() {
if(this.nodes.length > 0) { this._rebuildNodes(0);
return this.nodes[0].element; this.width = 0;
} else { this.height = 0;
return null;
}
} }
setText(newText) { _renderText() {
if(newText === this.text) { if(!this.state.text) {
this._reset();
return; return;
} }
if(!newText) {
this.clear();
return;
}
this.text = newText;
const lines = this.text.split('\n');
const lines = this.state.text.split('\n');
this._rebuildNodes(lines.length); this._rebuildNodes(lines.length);
let maxWidth = 0; let maxWidth = 0;
this.nodes.forEach(({text, element}, i) => { this.nodes.forEach(({text, element}, i) => {
if(text.nodeValue !== lines[i]) { if(text.nodeValue !== lines[i]) {
@ -87,25 +81,50 @@ define(['./SVGUtilities'], (svg) => {
this.width = maxWidth; this.width = maxWidth;
} }
reanchor(newX, newY) { _updateX() {
if(newX !== this.x) {
this.x = newX;
this.nodes.forEach(({element}) => { this.nodes.forEach(({element}) => {
element.setAttribute('x', this.x); element.setAttribute('x', this.state.x);
}); });
} }
if(newY !== this.y) { _updateY() {
this.y = newY; const {size, lineHeight} = fontDetails(this.state.attrs);
this._updateY(); this.nodes.forEach(({element}, i) => {
element.setAttribute('y', this.state.y + i * lineHeight + size);
});
this.height = lineHeight * this.nodes.length;
}
firstLine() {
if(this.nodes.length > 0) {
return this.nodes[0].element;
} else {
return null;
} }
} }
clear() { set(newState) {
this._rebuildNodes(0); const oldState = Object.assign({}, this.state);
this.text = ''; merge(this.state, newState);
this.width = 0;
this.height = 0; if(this.state.attrs !== oldState.attrs) {
this._reset();
oldState.text = '';
}
const oldNodes = this.nodes.length;
if(this.state.text !== oldState.text) {
this._renderText();
}
if(this.state.x !== oldState.x) {
this._updateX();
}
if(this.state.y !== oldState.y || this.nodes.length !== oldNodes) {
this._updateY();
}
} }
} }

View File

@ -14,7 +14,7 @@ defineDescribe('SVGTextBlock', [
beforeEach(() => { beforeEach(() => {
hold = svg.makeContainer(); hold = svg.makeContainer();
document.body.appendChild(hold); document.body.appendChild(hold);
block = new SVGTextBlock(hold, attrs); block = new SVGTextBlock(hold, {attrs});
}); });
afterEach(() => { afterEach(() => {
@ -23,52 +23,60 @@ defineDescribe('SVGTextBlock', [
describe('constructor', () => { describe('constructor', () => {
it('defaults to blank text at 0, 0', () => { it('defaults to blank text at 0, 0', () => {
expect(block.text).toEqual(''); expect(block.state.text).toEqual('');
expect(block.x).toEqual(0); expect(block.state.x).toEqual(0);
expect(block.y).toEqual(0); expect(block.state.y).toEqual(0);
expect(hold.children.length).toEqual(0);
});
it('does not explode if given no setup', () => {
block = new SVGTextBlock(hold);
expect(block.state.text).toEqual('');
expect(block.state.x).toEqual(0);
expect(block.state.y).toEqual(0);
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
}); });
it('adds the given text if specified', () => { it('adds the given text if specified', () => {
block = new SVGTextBlock(hold, attrs, {text: 'abc'}); block = new SVGTextBlock(hold, {attrs, text: 'abc'});
expect(block.text).toEqual('abc'); expect(block.state.text).toEqual('abc');
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
}); });
it('uses the given coordinates if specified', () => { it('uses the given coordinates if specified', () => {
block = new SVGTextBlock(hold, attrs, {x: 5, y: 7}); block = new SVGTextBlock(hold, {attrs, x: 5, y: 7});
expect(block.x).toEqual(5); expect(block.state.x).toEqual(5);
expect(block.y).toEqual(7); expect(block.state.y).toEqual(7);
}); });
}); });
describe('.setText', () => { describe('.set', () => {
it('sets the text to the given content', () => { it('sets the text to the given content', () => {
block.setText('foo'); block.set({text: 'foo'});
expect(block.text).toEqual('foo'); expect(block.state.text).toEqual('foo');
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
expect(hold.children[0].innerHTML).toEqual('foo'); expect(hold.children[0].innerHTML).toEqual('foo');
}); });
it('renders multiline text', () => { it('renders multiline text', () => {
block.setText('foo\nbar'); block.set({text: 'foo\nbar'});
expect(hold.children.length).toEqual(2); expect(hold.children.length).toEqual(2);
expect(hold.children[0].innerHTML).toEqual('foo'); expect(hold.children[0].innerHTML).toEqual('foo');
expect(hold.children[1].innerHTML).toEqual('bar'); expect(hold.children[1].innerHTML).toEqual('bar');
}); });
it('populates width and height with the size of the text', () => { it('populates width and height with the size of the text', () => {
block.setText('foo\nbar'); block.set({text: 'foo\nbar'});
expect(block.width).toBeGreaterThan(0); expect(block.width).toBeGreaterThan(0);
expect(block.height).toEqual(30); expect(block.height).toEqual(30);
}); });
it('re-uses text nodes when possible, adding more if needed', () => { it('re-uses text nodes when possible, adding more if needed', () => {
block.setText('foo\nbar'); block.set({text: 'foo\nbar'});
const line0 = hold.children[0]; const line0 = hold.children[0];
const line1 = hold.children[1]; const line1 = hold.children[1];
block.setText('zig\nzag\nbaz'); block.set({text: 'zig\nzag\nbaz'});
expect(hold.children.length).toEqual(3); expect(hold.children.length).toEqual(3);
expect(hold.children[0]).toEqual(line0); expect(hold.children[0]).toEqual(line0);
@ -79,10 +87,10 @@ defineDescribe('SVGTextBlock', [
}); });
it('re-uses text nodes when possible, removing extra if needed', () => { it('re-uses text nodes when possible, removing extra if needed', () => {
block.setText('foo\nbar'); block.set({text: 'foo\nbar'});
const line0 = hold.children[0]; const line0 = hold.children[0];
block.setText('zig'); block.set({text: 'zig'});
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
expect(hold.children[0]).toEqual(line0); expect(hold.children[0]).toEqual(line0);
@ -90,7 +98,7 @@ defineDescribe('SVGTextBlock', [
}); });
it('positions text nodes and applies attributes', () => { it('positions text nodes and applies attributes', () => {
block.setText('foo\nbar'); block.set({text: 'foo\nbar'});
expect(hold.children.length).toEqual(2); expect(hold.children.length).toEqual(2);
expect(hold.children[0].getAttribute('x')).toEqual('0'); expect(hold.children[0].getAttribute('x')).toEqual('0');
expect(hold.children[0].getAttribute('y')).toEqual('10'); expect(hold.children[0].getAttribute('y')).toEqual('10');
@ -99,25 +107,21 @@ defineDescribe('SVGTextBlock', [
expect(hold.children[1].getAttribute('y')).toEqual('25'); expect(hold.children[1].getAttribute('y')).toEqual('25');
expect(hold.children[1].getAttribute('font-size')).toEqual('10'); expect(hold.children[1].getAttribute('font-size')).toEqual('10');
}); });
});
describe('.reanchor', () => {
it('moves all nodes', () => { it('moves all nodes', () => {
block.setText('foo\nbaz'); block.set({text: 'foo\nbaz'});
block.reanchor(5, 7); block.set({x: 5, y: 7});
expect(hold.children[0].getAttribute('x')).toEqual('5'); expect(hold.children[0].getAttribute('x')).toEqual('5');
expect(hold.children[0].getAttribute('y')).toEqual('17'); expect(hold.children[0].getAttribute('y')).toEqual('17');
expect(hold.children[1].getAttribute('x')).toEqual('5'); expect(hold.children[1].getAttribute('x')).toEqual('5');
expect(hold.children[1].getAttribute('y')).toEqual('32'); expect(hold.children[1].getAttribute('y')).toEqual('32');
}); });
});
describe('.clear', () => { it('clears if the text is empty', () => {
it('resets the text empty', () => { block.set({text: 'foo\nbaz'});
block.setText('foo\nbaz'); block.set({text: ''});
block.setText('');
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
expect(block.text).toEqual(''); expect(block.state.text).toEqual('');
expect(block.width).toEqual(0); expect(block.width).toEqual(0);
expect(block.height).toEqual(0); expect(block.height).toEqual(0);
}); });