addition: added bounds models for bounds checking in unit tests.

addition: bounds.init will clear models
addition: added loop model widths id instead of using title as the id
removed parseMessage debug message
addition: added configuration labelBoxWidth and labelBoxHeight for loop label box width/height
change: adjusted svgDraw drawText to support anchor and valign and whether to output a tspan
change: drawText returns an array regardless of array size
change: hardcoded label width/height uses conf.labelBoxWidth and conf.labelBoxHeight
change: Math.round() on many of the calculations to clean up bounds values
change: getTextObj anchor, width, height start as undefined
This commit is contained in:
chris moran
2020-06-22 15:38:54 -04:00
parent ee109c2279
commit 7d9bf83f66
5 changed files with 279 additions and 214 deletions

View File

@@ -508,7 +508,7 @@ context('Sequence diagram', () => {
it('should overide config with directive settings', () => { it('should overide config with directive settings', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: { "sequence": { "mirrorActors": true }}}%% %%{init: { "config": { "mirrorActors": true }}}%%
sequenceDiagram sequenceDiagram
Alice->>Bob: I'm short Alice->>Bob: I'm short
note left of Alice: config set to mirrorActors: false<br/>directive set to mirrorActors: true note left of Alice: config set to mirrorActors: false<br/>directive set to mirrorActors: true
@@ -520,7 +520,7 @@ context('Sequence diagram', () => {
it('should overide config with directive settings', () => { it('should overide config with directive settings', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: { "sequence": { "mirrorActors": false }}}%% %%{init: { "config": { "mirrorActors": false }}}%%
sequenceDiagram sequenceDiagram
%%{config: { "mirrorActors": false} }%% %%{config: { "mirrorActors": false} }%%
Alice->>Bob: I'm short Alice->>Bob: I'm short

View File

@@ -202,7 +202,7 @@ export const clear = function() {
export const parseMessage = function(str) { export const parseMessage = function(str) {
const _str = str.trim(); const _str = str.trim();
const retVal = { return {
text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(), text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(),
wrap: wrap:
_str.match(/^[:]?(?:no)?wrap:/) === null _str.match(/^[:]?(?:no)?wrap:/) === null
@@ -213,8 +213,6 @@ export const parseMessage = function(str) {
? false ? false
: autoWrap() : autoWrap()
}; };
logger.debug(`ParseMessage[${str}] [${JSON.stringify(retVal, null, 2)}`);
return retVal;
}; };
export const LINETYPE = { export const LINETYPE = {

View File

@@ -46,6 +46,8 @@ const conf = {
// width of activation box // width of activation box
activationWidth: 10, activationWidth: 10,
labelBoxWidth: 50,
labelBoxHeight: 20,
// text placement as: tspan | fo | old only text as before // text placement as: tspan | fo | old only text as before
textPlacement: 'tspan', textPlacement: 'tspan',
@@ -91,9 +93,19 @@ export const bounds = {
sequenceItems: [], sequenceItems: [],
activations: [], activations: [],
models: {
loops: [],
messages: [],
notes: []
},
init: function() { init: function() {
this.sequenceItems = []; this.sequenceItems = [];
this.activations = []; this.activations = [];
this.models = {
loops: [],
messages: [],
notes: []
};
this.data = { this.data = {
startx: undefined, startx: undefined,
stopx: undefined, stopx: undefined,
@@ -181,6 +193,7 @@ export const bounds = {
title: title.message, title: title.message,
wrap: title.wrap, wrap: title.wrap,
width: title.width, width: title.width,
height: 0,
fill: fill fill: fill
}; };
}, },
@@ -194,7 +207,7 @@ export const bounds = {
const loop = this.sequenceItems.pop(); const loop = this.sequenceItems.pop();
loop.sections = loop.sections || []; loop.sections = loop.sections || [];
loop.sectionTitles = loop.sectionTitles || []; loop.sectionTitles = loop.sectionTitles || [];
loop.sections.push(bounds.getVerticalPos()); loop.sections.push({ y: bounds.getVerticalPos(), height: 0 });
loop.sectionTitles.push(message); loop.sectionTitles.push(message);
this.sequenceItems.push(loop); this.sequenceItems.push(loop);
}, },
@@ -210,58 +223,8 @@ export const bounds = {
} }
}; };
const drawLongText = (text, x, y, g, width) => {
const alignmentToAnchor = {
left: 'start',
start: 'start',
center: 'middle',
middle: 'middle',
right: 'end',
end: 'end'
};
const alignment = alignmentToAnchor[conf.noteAlign] || 'middle';
const textObj = svgDraw.getTextObj();
switch (alignment) {
case 'start':
textObj.x = x + conf.noteMargin;
break;
case 'middle':
textObj.x = x + width / 2;
break;
case 'end':
textObj.x = x + width - conf.noteMargin;
break;
}
textObj.y = y;
textObj.dy = '1em';
textObj.text = text;
textObj.class = 'noteText';
textObj.fontFamily = conf.noteFontFamily;
textObj.fontSize = conf.noteFontSize;
textObj.fontWeight = conf.noteFontWeight;
textObj.anchor = alignment;
textObj.textMargin = conf.noteMargin;
textObj.valign = alignment;
textObj.wrap = true;
let textElem = drawText(g, textObj);
if (!Array.isArray(textElem)) {
textElem = [textElem];
}
textElem.forEach(te => {
te.attr('dominant-baseline', 'central').attr('alignment-baseline', 'central');
});
return textElem
.map(te => (te._groups || te)[0][0].getBBox().height)
.reduce((acc, curr) => acc + curr);
};
/** /**
* Draws an note in the diagram with the attaced line * Draws an note in the diagram with the attached line
* @param elem - The diagram to draw to. * @param elem - The diagram to draw to.
* @param noteModel:{x: number, y: number, message: string, width: number} - startx: x axis start position, verticalPos: y axis position, messsage: the message to be shown, width: Set this with a custom width to override the default configured width. * @param noteModel:{x: number, y: number, message: string, width: number} - startx: x axis start position, verticalPos: y axis position, messsage: the message to be shown, width: Set this with a custom width to override the default configured width.
*/ */
@@ -274,8 +237,26 @@ const drawNote = function(elem, noteModel) {
let g = elem.append('g'); let g = elem.append('g');
const rectElem = svgDraw.drawRect(g, rect); const rectElem = svgDraw.drawRect(g, rect);
const textObj = svgDraw.getTextObj();
textObj.x = noteModel.x;
textObj.y = noteModel.y;
textObj.width = rect.width;
textObj.dy = '1em';
textObj.text = noteModel.message;
textObj.class = 'noteText';
textObj.fontFamily = conf.noteFontFamily;
textObj.fontSize = conf.noteFontSize;
textObj.fontWeight = conf.noteFontWeight;
textObj.anchor = conf.noteAlign;
textObj.textMargin = conf.noteMargin;
textObj.valign = conf.noteAlign;
textObj.wrap = true;
const textHeight = drawLongText(noteModel.message, noteModel.x, noteModel.y, g, rect.width); let textElem = drawText(g, textObj);
let textHeight = Math.round(
textElem.map(te => (te._groups || te)[0][0].getBBox().height).reduce((acc, curr) => acc + curr)
);
noteModel.height = textHeight + 2 * conf.noteMargin; noteModel.height = textHeight + 2 * conf.noteMargin;
@@ -287,52 +268,41 @@ const drawNote = function(elem, noteModel) {
/** /**
* Draws a message * Draws a message
* @param elem * @param g - the parent of the message element
* @param startx * @param msgModel - the model containing fields describing a message
* @param stopx
* @param verticalPos
* @param msg
* @param sequenceIndex
*/ */
const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) { const drawMessage = function(g, msgModel) {
const g = elem.append('g'); const { startx, stopx, starty: verticalPos, message, type, sequenceIndex, wrap } = msgModel;
const txtCenter = startx + (stopx - startx) / 2; const textObj = svgDraw.getTextObj();
textObj.x = startx;
textObj.y = verticalPos - (conf.messageFontSize + 7);
textObj.width = stopx - startx;
textObj.class = 'messageText';
textObj.dy = '1em';
textObj.text = message;
textObj.fontFamily = conf.messageFontFamily;
textObj.fontSize = conf.messageFontSize;
textObj.fontWeight = conf.messageFontWeight;
textObj.anchor = conf.messageAlign;
textObj.valign = conf.messageAlign;
textObj.textMargin = conf.wrapPadding;
textObj.tspan = false;
textObj.wrap = wrap;
let textElems = []; let textElem = drawText(g, textObj);
let counterBreaklines = 0; const lines = message.split(common.lineBreakRegex).length - 1 > 0 ? 1 : 0;
let breaklineOffset = conf.messageFontSize;
const breaklines = msg.message.split(common.lineBreakRegex);
for (const breakline of breaklines) {
textElems.push(
g
.append('text') // text label for the x axis
.attr('x', txtCenter)
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
.style('font-size', conf.messageFontSize)
.style('font-family', conf.messageFontFamily)
.style('font-weight', conf.messageFontWeight)
.style('text-anchor', 'middle')
.attr('class', 'messageText')
.text(breakline.trim())
);
counterBreaklines++;
}
const offsetLineCounter = counterBreaklines - 1;
let totalOffset = offsetLineCounter * breaklineOffset;
let textWidths = textElems.map(function(textElem) {
return (textElem._groups || textElem)[0][0].getBBox().width;
});
let textWidth = Math.max(...textWidths);
for (const textElem of textElems) {
if (conf.messageAlign === 'left') {
textElem.attr('x', txtCenter - textWidth / 2).style('text-anchor', 'start');
} else if (conf.messageAlign === 'right') {
textElem.attr('x', txtCenter + textWidth / 2).style('text-anchor', 'end');
}
}
bounds.bumpVerticalPos(totalOffset); let totalOffset = Math.round(
textElem.map(te => (te._groups || te)[0][0].getBBox().height).reduce((acc, curr) => acc + curr)
);
let textWidth = Math.max.apply(
null,
textElem.map(te => (te._groups || te)[0][0].getBBox().width)
);
bounds.bumpVerticalPos(totalOffset - lines * conf.messageFontSize);
let line; let line;
if (startx === stopx) { if (startx === stopx) {
@@ -371,7 +341,7 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
); );
} }
bounds.bumpVerticalPos(30 + totalOffset); bounds.bumpVerticalPos(30);
const dx = Math.max(textWidth / 2, 100); const dx = Math.max(textWidth / 2, 100);
bounds.insert( bounds.insert(
startx - dx, startx - dx,
@@ -395,9 +365,9 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
// Make an SVG Container // Make an SVG Container
// Draw the line // Draw the line
if ( if (
msg.type === parser.yy.LINETYPE.DOTTED || type === parser.yy.LINETYPE.DOTTED ||
msg.type === parser.yy.LINETYPE.DOTTED_CROSS || type === parser.yy.LINETYPE.DOTTED_CROSS ||
msg.type === parser.yy.LINETYPE.DOTTED_OPEN type === parser.yy.LINETYPE.DOTTED_OPEN
) { ) {
line.style('stroke-dasharray', '3, 3'); line.style('stroke-dasharray', '3, 3');
line.attr('class', 'messageLine1'); line.attr('class', 'messageLine1');
@@ -420,11 +390,11 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
line.attr('stroke-width', 2); line.attr('stroke-width', 2);
line.attr('stroke', 'black'); line.attr('stroke', 'black');
line.style('fill', 'none'); // remove any fill colour line.style('fill', 'none'); // remove any fill colour
if (msg.type === parser.yy.LINETYPE.SOLID || msg.type === parser.yy.LINETYPE.DOTTED) { if (type === parser.yy.LINETYPE.SOLID || type === parser.yy.LINETYPE.DOTTED) {
line.attr('marker-end', 'url(' + url + '#arrowhead)'); line.attr('marker-end', 'url(' + url + '#arrowhead)');
} }
if (msg.type === parser.yy.LINETYPE.SOLID_CROSS || msg.type === parser.yy.LINETYPE.DOTTED_CROSS) { if (type === parser.yy.LINETYPE.SOLID_CROSS || type === parser.yy.LINETYPE.DOTTED_CROSS) {
line.attr('marker-end', 'url(' + url + '#crosshead)'); line.attr('marker-end', 'url(' + url + '#crosshead)');
} }
@@ -452,8 +422,8 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
const actor = actors[actorKeys[i]]; const actor = actors[actorKeys[i]];
// Add some rendering data to the object // Add some rendering data to the object
actor.width = actor.width ? actor.width : conf.width; actor.width = actor.width || conf.width;
actor.height = conf.height; actor.height = Math.max(actor.height || conf.height, conf.height);
actor.margin = actor.margin || conf.actorMargin; actor.margin = actor.margin || conf.actorMargin;
actor.x = prevWidth + prevMargin; actor.x = prevWidth + prevMargin;
@@ -491,9 +461,9 @@ const actorActivations = function(actor) {
}); });
}; };
const actorFlowVerticaBounds = function(actor) { const actorFlowVerticaBounds = function(actor, actors) {
// handle multiple stacked activations for same actor // handle multiple stacked activations for same actor
const actorObj = parser.yy.getActors()[actor]; const actorObj = actors[actor];
const activations = actorActivations(actor); const activations = actorActivations(actor);
const left = activations.reduce(function(acc, activation) { const left = activations.reduce(function(acc, activation) {
@@ -506,28 +476,23 @@ const actorFlowVerticaBounds = function(actor) {
}; };
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) { function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
let heightAdjust = 0;
bounds.bumpVerticalPos(preMargin); bounds.bumpVerticalPos(preMargin);
if (msg.message && loopWidths[msg.message]) { let heightAdjust = postMargin;
let loopWidth = loopWidths[msg.message].width; if (msg.id && msg.message && loopWidths[msg.id]) {
let loopWidth = loopWidths[msg.id].width;
let textConf = conf.messageFont(); let textConf = conf.messageFont();
msg.message = utils.wrapLabel( msg.message = utils.wrapLabel(`[${msg.message}]`, loopWidth - 2 * conf.wrapPadding, textConf);
`[${msg.message}]`, msg.width = loopWidth;
loopWidth - 20 - 2 * conf.wrapPadding,
textConf
);
heightAdjust = Math.max( const textHeight = utils.calculateTextHeight(msg.message, textConf);
0, heightAdjust += textHeight;
utils.calculateTextHeight(msg.message, textConf) - (preMargin + postMargin)
);
} }
addLoopFn(msg); addLoopFn(msg);
bounds.bumpVerticalPos(heightAdjust + postMargin); bounds.bumpVerticalPos(heightAdjust);
} }
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
* @param text * @param text
* @param id * @param id
*/ */
@@ -576,15 +541,15 @@ export const draw = function(text, id) {
// Draw the messages/signals // Draw the messages/signals
let sequenceIndex = 1; let sequenceIndex = 1;
messages.forEach(function(msg) { messages.forEach(function(msg) {
let loopData, noteModel, msgModel; let loopModel, noteModel, msgModel;
switch (msg.type) { switch (msg.type) {
case parser.yy.LINETYPE.NOTE: case parser.yy.LINETYPE.NOTE:
bounds.bumpVerticalPos(conf.boxMargin); bounds.bumpVerticalPos(conf.boxMargin);
noteModel = msg.noteModel; noteModel = msg.noteModel;
noteModel.y = bounds.getVerticalPos(); noteModel.y = bounds.getVerticalPos();
logger.debug('noteModel', noteModel);
drawNote(diagram, noteModel); drawNote(diagram, noteModel);
bounds.models.notes.push(noteModel);
break; break;
case parser.yy.LINETYPE.ACTIVE_START: case parser.yy.LINETYPE.ACTIVE_START:
bounds.newActivation(msg, diagram, actors); bounds.newActivation(msg, diagram, actors);
@@ -602,18 +567,21 @@ export const draw = function(text, id) {
); );
break; break;
case parser.yy.LINETYPE.LOOP_END: case parser.yy.LINETYPE.LOOP_END:
loopData = bounds.endLoop(); loopModel = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'loop', conf); svgDraw.drawLoop(diagram, loopModel, 'loop', conf);
bounds.bumpVerticalPos(conf.boxMargin); bounds.models.loops.push(loopModel);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
break; break;
case parser.yy.LINETYPE.RECT_START: case parser.yy.LINETYPE.RECT_START:
bounds.bumpVerticalPos(conf.boxMargin); adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message =>
bounds.newLoop(undefined, msg.message); bounds.newLoop(undefined, message.message)
bounds.bumpVerticalPos(conf.boxMargin); );
break; break;
case parser.yy.LINETYPE.RECT_END: case parser.yy.LINETYPE.RECT_END:
svgDraw.drawBackgroundRect(diagram, bounds.endLoop()); loopModel = bounds.endLoop();
bounds.bumpVerticalPos(conf.boxMargin); svgDraw.drawBackgroundRect(diagram, loopModel);
bounds.models.loops.push(loopModel);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
break; break;
case parser.yy.LINETYPE.OPT_START: case parser.yy.LINETYPE.OPT_START:
adjustLoopHeightForWrap( adjustLoopHeightForWrap(
@@ -625,9 +593,10 @@ export const draw = function(text, id) {
); );
break; break;
case parser.yy.LINETYPE.OPT_END: case parser.yy.LINETYPE.OPT_END:
loopData = bounds.endLoop(); loopModel = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'opt', conf); svgDraw.drawLoop(diagram, loopModel, 'opt', conf);
bounds.bumpVerticalPos(conf.boxMargin); bounds.models.loops.push(loopModel);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
break; break;
case parser.yy.LINETYPE.ALT_START: case parser.yy.LINETYPE.ALT_START:
adjustLoopHeightForWrap( adjustLoopHeightForWrap(
@@ -639,14 +608,19 @@ export const draw = function(text, id) {
); );
break; break;
case parser.yy.LINETYPE.ALT_ELSE: case parser.yy.LINETYPE.ALT_ELSE:
adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message => adjustLoopHeightForWrap(
bounds.addSectionToLoop(message) loopWidths,
msg,
conf.boxMargin + conf.boxTextMargin,
conf.boxMargin,
message => bounds.addSectionToLoop(message)
); );
break; break;
case parser.yy.LINETYPE.ALT_END: case parser.yy.LINETYPE.ALT_END:
loopData = bounds.endLoop(); loopModel = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'alt', conf); svgDraw.drawLoop(diagram, loopModel, 'alt', conf);
bounds.bumpVerticalPos(conf.boxMargin); bounds.models.loops.push(loopModel);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
break; break;
case parser.yy.LINETYPE.PAR_START: case parser.yy.LINETYPE.PAR_START:
adjustLoopHeightForWrap( adjustLoopHeightForWrap(
@@ -658,14 +632,19 @@ export const draw = function(text, id) {
); );
break; break;
case parser.yy.LINETYPE.PAR_AND: case parser.yy.LINETYPE.PAR_AND:
adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message => adjustLoopHeightForWrap(
bounds.addSectionToLoop(message) loopWidths,
msg,
conf.boxMargin + conf.boxTextMargin,
conf.boxMargin,
message => bounds.addSectionToLoop(message)
); );
break; break;
case parser.yy.LINETYPE.PAR_END: case parser.yy.LINETYPE.PAR_END:
loopData = bounds.endLoop(); loopModel = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'par', conf); svgDraw.drawLoop(diagram, loopModel, 'par', conf);
bounds.bumpVerticalPos(conf.boxMargin); bounds.models.loops.push(loopModel);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
break; break;
default: default:
try { try {
@@ -673,14 +652,9 @@ export const draw = function(text, id) {
bounds.bumpVerticalPos(conf.messageMargin); bounds.bumpVerticalPos(conf.messageMargin);
msgModel = msg.msgModel; msgModel = msg.msgModel;
msgModel.starty = bounds.getVerticalPos(); msgModel.starty = bounds.getVerticalPos();
drawMessage( msgModel.sequenceIndex = sequenceIndex;
diagram, drawMessage(diagram, msgModel);
msgModel.startx, bounds.models.messages.push(msgModel);
msgModel.stopx,
msgModel.starty,
msgModel,
sequenceIndex
);
bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.starty); bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.starty);
} catch (e) { } catch (e) {
logger.error('error while drawing message', e); logger.error('error while drawing message', e);
@@ -750,6 +724,7 @@ export const draw = function(text, id) {
' ' + ' ' +
(height + extraVertForTitle) (height + extraVertForTitle)
); );
logger.debug('bounds', bounds);
}; };
/** /**
@@ -784,10 +759,10 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
const textFont = isNote ? conf.noteFont() : conf.messageFont(); const textFont = isNote ? conf.noteFont() : conf.messageFont();
let wrappedMessage = msg.wrap let wrappedMessage = msg.wrap
? utils.wrapLabel(msg.message, conf.width - conf.noteMargin, textFont) ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont)
: msg.message; : msg.message;
const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont); const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont);
const messageWidth = messageDimensions.width; const messageWidth = messageDimensions.width + 2 * conf.wrapPadding;
/* /*
* The following scenarios should be supported: * The following scenarios should be supported:
@@ -838,7 +813,7 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
} }
}); });
logger.debug('MaxMessages:', maxMessageWidthPerActor); logger.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor);
return maxMessageWidthPerActor; return maxMessageWidthPerActor;
}; };
@@ -915,7 +890,7 @@ const buildNoteModel = function(msg, actors) {
}; };
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) { if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
noteModel.width = shouldWrap noteModel.width = shouldWrap
? conf.width ? Math.max(conf.width, textDimensions.width)
: Math.max( : Math.max(
actors[msg.from].width / 2 + actors[msg.to].width / 2, actors[msg.from].width / 2 + actors[msg.to].width / 2,
textDimensions.width + 2 * conf.noteMargin textDimensions.width + 2 * conf.noteMargin
@@ -923,7 +898,7 @@ const buildNoteModel = function(msg, actors) {
noteModel.x = startx + (actors[msg.from].width + conf.actorMargin) / 2; noteModel.x = startx + (actors[msg.from].width + conf.actorMargin) / 2;
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) { } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
noteModel.width = shouldWrap noteModel.width = shouldWrap
? conf.width ? Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin)
: Math.max( : Math.max(
actors[msg.from].width / 2 + actors[msg.to].width / 2, actors[msg.from].width / 2 + actors[msg.to].width / 2,
textDimensions.width + 2 * conf.noteMargin textDimensions.width + 2 * conf.noteMargin
@@ -959,7 +934,7 @@ const buildNoteModel = function(msg, actors) {
return noteModel; return noteModel;
}; };
const buildMessageModel = function(msg) { const buildMessageModel = function(msg, actors) {
let process = false; let process = false;
if ( if (
[ [
@@ -976,13 +951,13 @@ const buildMessageModel = function(msg) {
if (!process) { if (!process) {
return {}; return {};
} }
const fromBounds = actorFlowVerticaBounds(msg.from); const fromBounds = actorFlowVerticaBounds(msg.from, actors);
const toBounds = actorFlowVerticaBounds(msg.to); const toBounds = actorFlowVerticaBounds(msg.to, actors);
const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0; const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1; const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
const allBounds = fromBounds.concat(toBounds); const allBounds = fromBounds.concat(toBounds);
const msgModel = { const msgModel = {
width: Math.abs(toBounds[toIdx] - fromBounds[fromIdx]) + conf.messageMargin * 2, width: Math.abs(toBounds[toIdx] - fromBounds[fromIdx]),
height: 0, height: 0,
startx: fromBounds[fromIdx], startx: fromBounds[fromIdx],
stopx: toBounds[toIdx], stopx: toBounds[toIdx],
@@ -990,13 +965,14 @@ const buildMessageModel = function(msg) {
stopy: 0, stopy: 0,
message: msg.message, message: msg.message,
type: msg.type, type: msg.type,
wrap: msg.wrap,
fromBounds: Math.min.apply(null, allBounds), fromBounds: Math.min.apply(null, allBounds),
toBounds: Math.max.apply(null, allBounds) toBounds: Math.max.apply(null, allBounds)
}; };
if (msg.wrap && msg.message && !common.lineBreakRegex.test(msg.message)) { if (msg.wrap && msg.message && !common.lineBreakRegex.test(msg.message)) {
msgModel.message = utils.wrapLabel( msgModel.message = utils.wrapLabel(
msg.message, msg.message,
Math.max(msgModel.width, conf.width + conf.messageMargin * 2), Math.max(msgModel.width, conf.width),
conf.messageFont() conf.messageFont()
); );
} }
@@ -1009,12 +985,14 @@ const calculateLoopBounds = function(messages, actors) {
let current, noteModel, msgModel; let current, noteModel, msgModel;
messages.forEach(function(msg) { messages.forEach(function(msg) {
msg.id = utils.generateId();
switch (msg.type) { switch (msg.type) {
case parser.yy.LINETYPE.LOOP_START: case parser.yy.LINETYPE.LOOP_START:
case parser.yy.LINETYPE.ALT_START: case parser.yy.LINETYPE.ALT_START:
case parser.yy.LINETYPE.OPT_START: case parser.yy.LINETYPE.OPT_START:
case parser.yy.LINETYPE.PAR_START: case parser.yy.LINETYPE.PAR_START:
stack.push({ stack.push({
id: msg.id,
msg: msg.message, msg: msg.message,
from: Number.MAX_SAFE_INTEGER, from: Number.MAX_SAFE_INTEGER,
to: Number.MIN_SAFE_INTEGER, to: Number.MIN_SAFE_INTEGER,
@@ -1025,7 +1003,8 @@ const calculateLoopBounds = function(messages, actors) {
case parser.yy.LINETYPE.PAR_AND: case parser.yy.LINETYPE.PAR_AND:
if (msg.message) { if (msg.message) {
current = stack.pop(); current = stack.pop();
loops[msg.message] = current; loops[current.id] = current;
loops[msg.id] = current;
stack.push(current); stack.push(current);
} }
break; break;
@@ -1034,23 +1013,52 @@ const calculateLoopBounds = function(messages, actors) {
case parser.yy.LINETYPE.OPT_END: case parser.yy.LINETYPE.OPT_END:
case parser.yy.LINETYPE.PAR_END: case parser.yy.LINETYPE.PAR_END:
current = stack.pop(); current = stack.pop();
loops[current.msg] = current; loops[current.id] = current;
break;
case parser.yy.LINETYPE.ACTIVE_START:
{
const actorRect = actors[msg.from.actor];
const stackedSize = actorActivations(msg.from.actor).length;
const x =
actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
const toAdd = {
startx: x,
stopx: x + conf.activationWidth,
actor: msg.from.actor,
enabled: true
};
bounds.activations.push(toAdd);
}
break;
case parser.yy.LINETYPE.ACTIVE_END:
{
const lastActorActivationIdx = bounds.activations
.map(a => a.actor)
.lastIndexOf(msg.from.actor);
delete bounds.activations.splice(lastActorActivationIdx, 1)[0];
}
break; break;
} }
const isNote = msg.placement !== undefined; const isNote = msg.placement !== undefined;
if (isNote) { if (isNote) {
noteModel = buildNoteModel(msg, actors); noteModel = buildNoteModel(msg, actors);
msg.noteModel = noteModel; msg.noteModel = noteModel;
let depth = 0;
stack.forEach(stk => { stack.forEach(stk => {
current = stk; current = stk;
current.from = Math.min(current.from, noteModel.x); current.from = Math.min(current.from, noteModel.x);
current.to = Math.max(current.to, noteModel.x + noteModel.width); current.to = Math.max(current.to, noteModel.x + noteModel.width);
current.width = Math.max(current.width, Math.abs(current.from - current.to)); current.width =
Math.max(current.width, Math.abs(current.from - current.to)) -
50 -
conf.boxMargin * depth;
depth++;
}); });
} else { } else {
msgModel = buildMessageModel(msg); msgModel = buildMessageModel(msg, actors);
msg.msgModel = msgModel; msg.msgModel = msgModel;
if (msg.from && msg.to && stack.length > 0) { if (msg.from && msg.to && stack.length > 0) {
let depth = 0;
stack.forEach(stk => { stack.forEach(stk => {
current = stk; current = stk;
let from = actors[msg.from]; let from = actors[msg.from];
@@ -1058,7 +1066,7 @@ const calculateLoopBounds = function(messages, actors) {
if (from.x === to.x) { if (from.x === to.x) {
current.from = Math.min(current.from, from.x); current.from = Math.min(current.from, from.x);
current.to = Math.max(current.to, to.x); current.to = Math.max(current.to, to.x);
current.width = Math.max(current.width, from.width); current.width = Math.max(current.width, from.width) - 50 - conf.boxMargin * depth;
} else { } else {
if (from.x < to.x) { if (from.x < to.x) {
current.from = Math.min(current.from, from.x); current.from = Math.min(current.from, from.x);
@@ -1067,15 +1075,17 @@ const calculateLoopBounds = function(messages, actors) {
current.from = Math.min(current.from, to.x); current.from = Math.min(current.from, to.x);
current.to = Math.max(current.to, from.x); current.to = Math.max(current.to, from.x);
} }
current.width = Math.max( current.width =
current.width, Math.max(current.width, Math.abs(current.from - current.to)) -
Math.abs(current.from - current.to) - 20 + 2 * conf.wrapPadding 50 -
); conf.boxMargin * depth;
} }
depth++;
}); });
} }
} }
}); });
bounds.activations = [];
logger.debug('Loop type widths:', loops); logger.debug('Loop type widths:', loops);
return loops; return loops;
}; };

View File

@@ -51,6 +51,35 @@ export const drawText = function(elem, textData) {
break; break;
} }
} }
if (
typeof textData.anchor !== 'undefined' &&
typeof textData.textMargin !== 'undefined' &&
typeof textData.width !== 'undefined'
) {
switch (textData.anchor) {
case 'left':
case 'start':
textData.x = textData.x + textData.textMargin;
textData.anchor = 'start';
textData.dominantBaseline = 'text-after-edge';
textData.alignmentBaseline = 'middle';
break;
case 'middle':
case 'center':
textData.x = textData.x + textData.width / 2;
textData.anchor = 'middle';
textData.dominantBaseline = 'middle';
textData.alignmentBaseline = 'middle';
break;
case 'right':
case 'end':
textData.x = textData.x + textData.width - textData.textMargin;
textData.anchor = 'end';
textData.dominantBaseline = 'text-before-edge';
textData.alignmentBaseline = 'middle';
break;
}
}
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let line = lines[i]; let line = lines[i];
if ( if (
@@ -65,7 +94,10 @@ export const drawText = function(elem, textData) {
textElem.attr('x', textData.x); textElem.attr('x', textData.x);
textElem.attr('y', yfunc()); textElem.attr('y', yfunc());
if (typeof textData.anchor !== 'undefined') { if (typeof textData.anchor !== 'undefined') {
textElem.style('text-anchor', textData.anchor); textElem
.attr('text-anchor', textData.anchor)
.attr('dominant-baseline', textData.dominantBaseline)
.attr('alignment-baseline', textData.alignmentBaseline);
} }
if (typeof textData.fontFamily !== 'undefined') { if (typeof textData.fontFamily !== 'undefined') {
textElem.style('font-family', textData.fontFamily); textElem.style('font-family', textData.fontFamily);
@@ -88,13 +120,16 @@ export const drawText = function(elem, textData) {
textElem.attr('dy', dy); textElem.attr('dy', dy);
} }
const span = textElem.append('tspan'); if (textData.tspan) {
span.attr('x', textData.x); const span = textElem.append('tspan');
if (typeof textData.fill !== 'undefined') { span.attr('x', textData.x);
span.attr('fill', textData.fill); if (typeof textData.fill !== 'undefined') {
span.attr('fill', textData.fill);
}
span.text(line);
} else {
textElem.text(line);
} }
span.text(line);
if ( if (
typeof textData.valign !== 'undefined' && typeof textData.valign !== 'undefined' &&
typeof textData.textMargin !== 'undefined' && typeof textData.textMargin !== 'undefined' &&
@@ -107,7 +142,7 @@ export const drawText = function(elem, textData) {
textElems.push(textElem); textElems.push(textElem);
} }
return textElems.length === 1 ? textElems[0] : textElems; return textElems;
}; };
export const drawLabel = function(elem, txtObject) { export const drawLabel = function(elem, txtObject) {
@@ -135,17 +170,18 @@ export const drawLabel = function(elem, txtObject) {
); );
} }
const polygon = elem.append('polygon'); const polygon = elem.append('polygon');
polygon.attr('points', genPoints(txtObject.x, txtObject.y, 50, 20, 7)); polygon.attr('points', genPoints(txtObject.x, txtObject.y, txtObject.width, txtObject.height, 7));
polygon.attr('class', 'labelBox'); polygon.attr('class', 'labelBox');
txtObject.y = txtObject.y + txtObject.labelMargin; txtObject.y = txtObject.y + txtObject.height / 2;
txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin;
return drawText(elem, txtObject); drawText(elem, txtObject);
return polygon;
}; };
let actorCnt = -1; let actorCnt = -1;
/** /**
* Draws an actor in the diagram with the attaced line * Draws an actor in the diagram with the attached line
* @param elem - The diagram we'll draw to. * @param elem - The diagram we'll draw to.
* @param actor - The actor to draw. * @param actor - The actor to draw.
* @param conf - drawText implementation discriminator object * @param conf - drawText implementation discriminator object
@@ -236,57 +272,67 @@ export const drawLoop = function(elem, bounds, labelText, conf) {
drawLoopLine(bounds.startx, bounds.starty, bounds.startx, bounds.stopy); drawLoopLine(bounds.startx, bounds.starty, bounds.startx, bounds.stopy);
if (typeof bounds.sections !== 'undefined') { if (typeof bounds.sections !== 'undefined') {
bounds.sections.forEach(function(item) { bounds.sections.forEach(function(item) {
drawLoopLine(bounds.startx, item, bounds.stopx, item).style('stroke-dasharray', '3, 3'); drawLoopLine(bounds.startx, item.y, bounds.stopx, item.y).style('stroke-dasharray', '3, 3');
}); });
} }
let minSize =
Math.round((3 * conf.messageFontSize) / 4) < 10
? conf.messageFontSize
: Math.round((3 * conf.messageFontSize) / 4);
let txt = getTextObj(); let txt = getTextObj();
txt.text = labelText; txt.text = labelText;
txt.x = bounds.startx; txt.x = bounds.startx;
txt.y = bounds.starty; txt.y = bounds.starty;
txt.labelMargin = 1.5 * 10; // This is the small box that says "loop" const msgFont = conf.messageFont();
txt.fontFamily = conf.messageFontFamily; txt.fontFamily = msgFont.fontFamily;
txt.fontSize = minSize; txt.fontSize = msgFont.fontSize;
txt.fontWeight = conf.messageFontWeight; txt.fontWeight = msgFont.fontWeight;
txt.class = 'labelText'; // Its size & position are fixed. txt.anchor = 'middle';
txt.valign = 'middle';
txt.tspan = false;
txt.width = conf.labelBoxWidth || 50;
txt.height = conf.labelBoxHeight || 20;
txt.textMargin = conf.boxTextMargin;
txt.class = 'labelText';
let labelElem = drawLabel(g, txt); drawLabel(g, txt);
let labelBoxWidth = (labelElem._groups || labelElem)[0][0].getBBox().width;
txt = getTextObj(); txt = getTextObj();
txt.text = bounds.title; txt.text = bounds.title;
txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2 + labelBoxWidth; txt.x = bounds.startx + conf.labelBoxWidth / 2 + (bounds.stopx - bounds.startx) / 2;
txt.y = bounds.starty + conf.boxMargin + conf.boxTextMargin; txt.y = bounds.starty + conf.boxMargin + conf.boxTextMargin;
txt.anchor = 'middle'; txt.anchor = 'middle';
txt.class = 'loopText'; txt.class = 'loopText';
txt.fontFamily = conf.messageFontFamily; txt.fontFamily = msgFont.fontFamily;
txt.fontSize = minSize; txt.fontSize = msgFont.fontSize;
txt.fontWeight = conf.messageFontWeight; txt.fontWeight = msgFont.fontWeight;
txt.wrap = true; txt.wrap = true;
drawText(g, txt); let textElem = drawText(g, txt);
if (typeof bounds.sectionTitles !== 'undefined') { if (typeof bounds.sectionTitles !== 'undefined') {
bounds.sectionTitles.forEach(function(item, idx) { bounds.sectionTitles.forEach(function(item, idx) {
if (item.message) { if (item.message) {
txt.text = item.message; txt.text = item.message;
txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2; txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2;
txt.y = bounds.sections[idx] + conf.boxMargin + conf.boxTextMargin; txt.y = bounds.sections[idx].y + conf.boxMargin + conf.boxTextMargin;
txt.class = 'loopText'; txt.class = 'loopText';
txt.anchor = 'middle'; txt.anchor = 'middle';
txt.fontFamily = conf.messageFontFamily; txt.valign = 'middle';
txt.fontSize = minSize; txt.tspan = false;
txt.fontWeight = conf.messageFontWeight; txt.fontFamily = msgFont.fontFamily;
txt.fontSize = msgFont.fontSize;
txt.fontWeight = msgFont.fontWeight;
txt.wrap = bounds.wrap; txt.wrap = bounds.wrap;
drawText(g, txt); textElem = drawText(g, txt);
let sectionHeight = Math.round(
textElem
.map(te => (te._groups || te)[0][0].getBBox().height)
.reduce((acc, curr) => acc + curr)
);
bounds.sections[idx].height += sectionHeight - (conf.boxMargin + conf.boxTextMargin);
} }
}); });
} }
bounds.height = Math.round(bounds.stopy - bounds.starty);
return g; return g;
}; };
@@ -380,13 +426,14 @@ export const getTextObj = function() {
x: 0, x: 0,
y: 0, y: 0,
fill: undefined, fill: undefined,
anchor: 'start', anchor: undefined,
style: '#666', style: '#666',
width: 100, width: undefined,
height: 100, height: undefined,
textMargin: 0, textMargin: 0,
rx: 0, rx: 0,
ry: 0, ry: 0,
tspan: true,
valign: undefined valign: undefined
}; };
}; };

View File

@@ -335,7 +335,17 @@ const config = {
* This sets the auto-wrap padding for the diagram (sides only) * This sets the auto-wrap padding for the diagram (sides only)
* **Default value 15. * **Default value 15.
*/ */
wrapPadding: 15 wrapPadding: 15,
/**
* This sets the width of the loop-box (loop, alt, opt, par)
* **Default value 50.
*/
labelBoxWidth: 50,
/**
* This sets the height of the loop-box (loop, alt, opt, par)
* **Default value 20.
*/
labelBoxHeight: 20
}, },
/** /**