mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-13 12:29:42 +02:00
1310 lines
42 KiB
JavaScript
1310 lines
42 KiB
JavaScript
import { select, selectAll } from 'd3';
|
|
import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw';
|
|
import { log } from '../../logger';
|
|
import { parser } from './parser/sequenceDiagram';
|
|
import common from '../common/common';
|
|
import sequenceDb from './sequenceDb';
|
|
import * as configApi from '../../config';
|
|
import utils, { assignWithDepth, configureSvgSize } from '../../utils';
|
|
import addSVGAccessibilityFields from '../../accessibility';
|
|
|
|
parser.yy = sequenceDb;
|
|
|
|
let conf = {};
|
|
|
|
export const bounds = {
|
|
data: {
|
|
startx: undefined,
|
|
stopx: undefined,
|
|
starty: undefined,
|
|
stopy: undefined,
|
|
},
|
|
verticalPos: 0,
|
|
sequenceItems: [],
|
|
activations: [],
|
|
models: {
|
|
getHeight: function () {
|
|
return (
|
|
Math.max.apply(
|
|
null,
|
|
this.actors.length === 0 ? [0] : this.actors.map((actor) => actor.height || 0)
|
|
) +
|
|
(this.loops.length === 0
|
|
? 0
|
|
: this.loops.map((it) => it.height || 0).reduce((acc, h) => acc + h)) +
|
|
(this.messages.length === 0
|
|
? 0
|
|
: this.messages.map((it) => it.height || 0).reduce((acc, h) => acc + h)) +
|
|
(this.notes.length === 0
|
|
? 0
|
|
: this.notes.map((it) => it.height || 0).reduce((acc, h) => acc + h))
|
|
);
|
|
},
|
|
clear: function () {
|
|
this.actors = [];
|
|
this.loops = [];
|
|
this.messages = [];
|
|
this.notes = [];
|
|
},
|
|
addActor: function (actorModel) {
|
|
this.actors.push(actorModel);
|
|
},
|
|
addLoop: function (loopModel) {
|
|
this.loops.push(loopModel);
|
|
},
|
|
addMessage: function (msgModel) {
|
|
this.messages.push(msgModel);
|
|
},
|
|
addNote: function (noteModel) {
|
|
this.notes.push(noteModel);
|
|
},
|
|
lastActor: function () {
|
|
return this.actors[this.actors.length - 1];
|
|
},
|
|
lastLoop: function () {
|
|
return this.loops[this.loops.length - 1];
|
|
},
|
|
lastMessage: function () {
|
|
return this.messages[this.messages.length - 1];
|
|
},
|
|
lastNote: function () {
|
|
return this.notes[this.notes.length - 1];
|
|
},
|
|
actors: [],
|
|
loops: [],
|
|
messages: [],
|
|
notes: [],
|
|
},
|
|
init: function () {
|
|
this.sequenceItems = [];
|
|
this.activations = [];
|
|
this.models.clear();
|
|
this.data = {
|
|
startx: undefined,
|
|
stopx: undefined,
|
|
starty: undefined,
|
|
stopy: undefined,
|
|
};
|
|
this.verticalPos = 0;
|
|
setConf(parser.yy.getConfig());
|
|
},
|
|
updateVal: function (obj, key, val, fun) {
|
|
if (typeof obj[key] === 'undefined') {
|
|
obj[key] = val;
|
|
} else {
|
|
obj[key] = fun(val, obj[key]);
|
|
}
|
|
},
|
|
updateBounds: function (startx, starty, stopx, stopy) {
|
|
const _self = this;
|
|
let cnt = 0;
|
|
/** @param {any} type */
|
|
function updateFn(type) {
|
|
return function updateItemBounds(item) {
|
|
cnt++;
|
|
// The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems
|
|
const n = _self.sequenceItems.length - cnt + 1;
|
|
|
|
_self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min);
|
|
_self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max);
|
|
|
|
_self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min);
|
|
_self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max);
|
|
|
|
if (!(type === 'activation')) {
|
|
_self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min);
|
|
_self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max);
|
|
|
|
_self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min);
|
|
_self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max);
|
|
}
|
|
};
|
|
}
|
|
|
|
this.sequenceItems.forEach(updateFn());
|
|
this.activations.forEach(updateFn('activation'));
|
|
},
|
|
insert: function (startx, starty, stopx, stopy) {
|
|
const _startx = Math.min(startx, stopx);
|
|
const _stopx = Math.max(startx, stopx);
|
|
const _starty = Math.min(starty, stopy);
|
|
const _stopy = Math.max(starty, stopy);
|
|
|
|
this.updateVal(bounds.data, 'startx', _startx, Math.min);
|
|
this.updateVal(bounds.data, 'starty', _starty, Math.min);
|
|
this.updateVal(bounds.data, 'stopx', _stopx, Math.max);
|
|
this.updateVal(bounds.data, 'stopy', _stopy, Math.max);
|
|
|
|
this.updateBounds(_startx, _starty, _stopx, _stopy);
|
|
},
|
|
newActivation: function (message, diagram, actors) {
|
|
const actorRect = actors[message.from.actor];
|
|
const stackedSize = actorActivations(message.from.actor).length || 0;
|
|
const x = actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
|
|
this.activations.push({
|
|
startx: x,
|
|
starty: this.verticalPos + 2,
|
|
stopx: x + conf.activationWidth,
|
|
stopy: undefined,
|
|
actor: message.from.actor,
|
|
anchored: svgDraw.anchorElement(diagram),
|
|
});
|
|
},
|
|
endActivation: function (message) {
|
|
// find most recent activation for given actor
|
|
const lastActorActivationIdx = this.activations
|
|
.map(function (activation) {
|
|
return activation.actor;
|
|
})
|
|
.lastIndexOf(message.from.actor);
|
|
return this.activations.splice(lastActorActivationIdx, 1)[0];
|
|
},
|
|
createLoop: function (title = { message: undefined, wrap: false, width: undefined }, fill) {
|
|
return {
|
|
startx: undefined,
|
|
starty: this.verticalPos,
|
|
stopx: undefined,
|
|
stopy: undefined,
|
|
title: title.message,
|
|
wrap: title.wrap,
|
|
width: title.width,
|
|
height: 0,
|
|
fill: fill,
|
|
};
|
|
},
|
|
newLoop: function (title = { message: undefined, wrap: false, width: undefined }, fill) {
|
|
this.sequenceItems.push(this.createLoop(title, fill));
|
|
},
|
|
endLoop: function () {
|
|
return this.sequenceItems.pop();
|
|
},
|
|
addSectionToLoop: function (message) {
|
|
const loop = this.sequenceItems.pop();
|
|
loop.sections = loop.sections || [];
|
|
loop.sectionTitles = loop.sectionTitles || [];
|
|
loop.sections.push({ y: bounds.getVerticalPos(), height: 0 });
|
|
loop.sectionTitles.push(message);
|
|
this.sequenceItems.push(loop);
|
|
},
|
|
bumpVerticalPos: function (bump) {
|
|
this.verticalPos = this.verticalPos + bump;
|
|
this.data.stopy = this.verticalPos;
|
|
},
|
|
getVerticalPos: function () {
|
|
return this.verticalPos;
|
|
},
|
|
getBounds: function () {
|
|
return { bounds: this.data, models: this.models };
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Draws an note in the diagram with the attached line
|
|
*
|
|
* @param {any} elem - The diagram to draw to.
|
|
* @param {{ x: number; y: number; message: string; width: number }} noteModel - 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.
|
|
*/
|
|
const drawNote = function (elem, noteModel) {
|
|
bounds.bumpVerticalPos(conf.boxMargin);
|
|
noteModel.height = conf.boxMargin;
|
|
noteModel.starty = bounds.getVerticalPos();
|
|
const rect = svgDraw.getNoteRect();
|
|
rect.x = noteModel.startx;
|
|
rect.y = noteModel.starty;
|
|
rect.width = noteModel.width || conf.width;
|
|
rect.class = 'note';
|
|
|
|
let g = elem.append('g');
|
|
const rectElem = svgDraw.drawRect(g, rect);
|
|
const textObj = svgDraw.getTextObj();
|
|
textObj.x = noteModel.startx;
|
|
textObj.y = noteModel.starty;
|
|
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;
|
|
|
|
let textElem = drawText(g, textObj);
|
|
|
|
let textHeight = Math.round(
|
|
textElem
|
|
.map((te) => (te._groups || te)[0][0].getBBox().height)
|
|
.reduce((acc, curr) => acc + curr)
|
|
);
|
|
|
|
rectElem.attr('height', textHeight + 2 * conf.noteMargin);
|
|
noteModel.height += textHeight + 2 * conf.noteMargin;
|
|
bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin);
|
|
noteModel.stopy = noteModel.starty + textHeight + 2 * conf.noteMargin;
|
|
noteModel.stopx = noteModel.startx + rect.width;
|
|
bounds.insert(noteModel.startx, noteModel.starty, noteModel.stopx, noteModel.stopy);
|
|
bounds.models.addNote(noteModel);
|
|
};
|
|
|
|
const messageFont = (cnf) => {
|
|
return {
|
|
fontFamily: cnf.messageFontFamily,
|
|
fontSize: cnf.messageFontSize,
|
|
fontWeight: cnf.messageFontWeight,
|
|
};
|
|
};
|
|
const noteFont = (cnf) => {
|
|
return {
|
|
fontFamily: cnf.noteFontFamily,
|
|
fontSize: cnf.noteFontSize,
|
|
fontWeight: cnf.noteFontWeight,
|
|
};
|
|
};
|
|
const actorFont = (cnf) => {
|
|
return {
|
|
fontFamily: cnf.actorFontFamily,
|
|
fontSize: cnf.actorFontSize,
|
|
fontWeight: cnf.actorFontWeight,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Process a message by adding its dimensions to the bound. It returns the Y coordinate of the
|
|
* message so it can be drawn later. We do not draw the message at this point so the arrowhead can
|
|
* be on top of the activation box.
|
|
*
|
|
* @param {any} diagram - The parent of the message element
|
|
* @param {any} msgModel - The model containing fields describing a message
|
|
* @returns {number} LineStarty - The Y coordinate at which the message line starts
|
|
*/
|
|
const boundMessage = function (diagram, msgModel) {
|
|
bounds.bumpVerticalPos(10);
|
|
const { startx, stopx, message } = msgModel;
|
|
const lines = common.splitBreaks(message).length;
|
|
let textDims = utils.calculateTextDimensions(message, messageFont(conf));
|
|
const lineHeight = textDims.height / lines;
|
|
msgModel.height += lineHeight;
|
|
|
|
bounds.bumpVerticalPos(lineHeight);
|
|
|
|
let lineStarty;
|
|
let totalOffset = textDims.height - 10;
|
|
let textWidth = textDims.width;
|
|
|
|
if (startx === stopx) {
|
|
lineStarty = bounds.getVerticalPos() + totalOffset;
|
|
if (!conf.rightAngles) {
|
|
totalOffset += conf.boxMargin;
|
|
lineStarty = bounds.getVerticalPos() + totalOffset;
|
|
}
|
|
totalOffset += 30;
|
|
const dx = Math.max(textWidth / 2, conf.width / 2);
|
|
bounds.insert(
|
|
startx - dx,
|
|
bounds.getVerticalPos() - 10 + totalOffset,
|
|
stopx + dx,
|
|
bounds.getVerticalPos() + 30 + totalOffset
|
|
);
|
|
} else {
|
|
totalOffset += conf.boxMargin;
|
|
lineStarty = bounds.getVerticalPos() + totalOffset;
|
|
bounds.insert(startx, lineStarty - 10, stopx, lineStarty);
|
|
}
|
|
bounds.bumpVerticalPos(totalOffset);
|
|
msgModel.height += totalOffset;
|
|
msgModel.stopy = msgModel.starty + msgModel.height;
|
|
bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.stopy);
|
|
|
|
return lineStarty;
|
|
};
|
|
|
|
/**
|
|
* Draws a message. Note that the bounds have previously been updated by boundMessage.
|
|
*
|
|
* @param {any} diagram - The parent of the message element
|
|
* @param {any} msgModel - The model containing fields describing a message
|
|
* @param {float} lineStarty - The Y coordinate at which the message line starts
|
|
*/
|
|
const drawMessage = function (diagram, msgModel, lineStarty) {
|
|
const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel;
|
|
let textDims = utils.calculateTextDimensions(message, messageFont(conf));
|
|
const textObj = svgDraw.getTextObj();
|
|
textObj.x = startx;
|
|
textObj.y = starty + 10;
|
|
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;
|
|
|
|
drawText(diagram, textObj);
|
|
|
|
let textWidth = textDims.width;
|
|
|
|
let line;
|
|
if (startx === stopx) {
|
|
if (conf.rightAngles) {
|
|
line = diagram
|
|
.append('path')
|
|
.attr(
|
|
'd',
|
|
`M ${startx},${lineStarty} H ${startx + Math.max(conf.width / 2, textWidth / 2)} V ${
|
|
lineStarty + 25
|
|
} H ${startx}`
|
|
);
|
|
} else {
|
|
line = diagram
|
|
.append('path')
|
|
.attr(
|
|
'd',
|
|
'M ' +
|
|
startx +
|
|
',' +
|
|
lineStarty +
|
|
' C ' +
|
|
(startx + 60) +
|
|
',' +
|
|
(lineStarty - 10) +
|
|
' ' +
|
|
(startx + 60) +
|
|
',' +
|
|
(lineStarty + 30) +
|
|
' ' +
|
|
startx +
|
|
',' +
|
|
(lineStarty + 20)
|
|
);
|
|
}
|
|
} else {
|
|
line = diagram.append('line');
|
|
line.attr('x1', startx);
|
|
line.attr('y1', lineStarty);
|
|
line.attr('x2', stopx);
|
|
line.attr('y2', lineStarty);
|
|
}
|
|
// Make an SVG Container
|
|
// Draw the line
|
|
if (
|
|
type === parser.yy.LINETYPE.DOTTED ||
|
|
type === parser.yy.LINETYPE.DOTTED_CROSS ||
|
|
type === parser.yy.LINETYPE.DOTTED_POINT ||
|
|
type === parser.yy.LINETYPE.DOTTED_OPEN
|
|
) {
|
|
line.style('stroke-dasharray', '3, 3');
|
|
line.attr('class', 'messageLine1');
|
|
} else {
|
|
line.attr('class', 'messageLine0');
|
|
}
|
|
|
|
let url = '';
|
|
if (conf.arrowMarkerAbsolute) {
|
|
url =
|
|
window.location.protocol +
|
|
'//' +
|
|
window.location.host +
|
|
window.location.pathname +
|
|
window.location.search;
|
|
url = url.replace(/\(/g, '\\(');
|
|
url = url.replace(/\)/g, '\\)');
|
|
}
|
|
|
|
line.attr('stroke-width', 2);
|
|
line.attr('stroke', 'none'); // handled by theme/css anyway
|
|
line.style('fill', 'none'); // remove any fill colour
|
|
if (type === parser.yy.LINETYPE.SOLID || type === parser.yy.LINETYPE.DOTTED) {
|
|
line.attr('marker-end', 'url(' + url + '#arrowhead)');
|
|
}
|
|
if (type === parser.yy.LINETYPE.SOLID_POINT || type === parser.yy.LINETYPE.DOTTED_POINT) {
|
|
line.attr('marker-end', 'url(' + url + '#filled-head)');
|
|
}
|
|
|
|
if (type === parser.yy.LINETYPE.SOLID_CROSS || type === parser.yy.LINETYPE.DOTTED_CROSS) {
|
|
line.attr('marker-end', 'url(' + url + '#crosshead)');
|
|
}
|
|
|
|
// add node number
|
|
if (sequenceVisible || conf.showSequenceNumbers) {
|
|
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
|
diagram
|
|
.append('text')
|
|
.attr('x', startx)
|
|
.attr('y', lineStarty + 4)
|
|
.attr('font-family', 'sans-serif')
|
|
.attr('font-size', '12px')
|
|
.attr('text-anchor', 'middle')
|
|
.attr('textLength', '16px')
|
|
.attr('class', 'sequenceNumber')
|
|
.text(sequenceIndex);
|
|
}
|
|
};
|
|
|
|
export const drawActors = function (
|
|
diagram,
|
|
actors,
|
|
actorKeys,
|
|
verticalPos,
|
|
configuration,
|
|
messages
|
|
) {
|
|
if (configuration.hideUnusedParticipants === true) {
|
|
const newActors = new Set();
|
|
messages.forEach((message) => {
|
|
newActors.add(message.from);
|
|
newActors.add(message.to);
|
|
});
|
|
actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey));
|
|
}
|
|
|
|
// Draw the actors
|
|
let prevWidth = 0;
|
|
let prevMargin = 0;
|
|
let maxHeight = 0;
|
|
for (let i = 0; i < actorKeys.length; i++) {
|
|
const actor = actors[actorKeys[i]];
|
|
|
|
// Add some rendering data to the object
|
|
actor.width = actor.width || conf.width;
|
|
actor.height = Math.max(actor.height || conf.height, conf.height);
|
|
actor.margin = actor.margin || conf.actorMargin;
|
|
|
|
actor.x = prevWidth + prevMargin;
|
|
actor.y = verticalPos;
|
|
|
|
// Draw the box with the attached line
|
|
const height = svgDraw.drawActor(diagram, actor, conf);
|
|
maxHeight = Math.max(maxHeight, height);
|
|
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
|
|
|
|
prevWidth += actor.width;
|
|
prevMargin += actor.margin;
|
|
bounds.models.addActor(actor);
|
|
}
|
|
|
|
// Add a margin between the actor boxes and the first arrow
|
|
bounds.bumpVerticalPos(maxHeight);
|
|
};
|
|
|
|
export const drawActorsPopup = function (diagram, actors, actorKeys, doc) {
|
|
var maxHeight = 0;
|
|
var maxWidth = 0;
|
|
for (let i = 0; i < actorKeys.length; i++) {
|
|
const actor = actors[actorKeys[i]];
|
|
const minMenuWidth = getRequiredPopupWidth(actor);
|
|
var menuDimensions = svgDraw.drawPopup(
|
|
diagram,
|
|
actor,
|
|
minMenuWidth,
|
|
conf,
|
|
conf.forceMenus,
|
|
doc
|
|
);
|
|
if (menuDimensions.height > maxHeight) {
|
|
maxHeight = menuDimensions.height;
|
|
}
|
|
if (menuDimensions.width + actor.x > maxWidth) {
|
|
maxWidth = menuDimensions.width + actor.x;
|
|
}
|
|
}
|
|
|
|
return { maxHeight: maxHeight, maxWidth: maxWidth };
|
|
};
|
|
|
|
export const setConf = function (cnf) {
|
|
assignWithDepth(conf, cnf);
|
|
|
|
if (cnf.fontFamily) {
|
|
conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
|
|
}
|
|
if (cnf.fontSize) {
|
|
conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
|
|
}
|
|
if (cnf.fontWeight) {
|
|
conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight;
|
|
}
|
|
};
|
|
|
|
const actorActivations = function (actor) {
|
|
return bounds.activations.filter(function (activation) {
|
|
return activation.actor === actor;
|
|
});
|
|
};
|
|
|
|
const activationBounds = function (actor, actors) {
|
|
// handle multiple stacked activations for same actor
|
|
const actorObj = actors[actor];
|
|
const activations = actorActivations(actor);
|
|
|
|
const left = activations.reduce(function (acc, activation) {
|
|
return Math.min(acc, activation.startx);
|
|
}, actorObj.x + actorObj.width / 2);
|
|
const right = activations.reduce(function (acc, activation) {
|
|
return Math.max(acc, activation.stopx);
|
|
}, actorObj.x + actorObj.width / 2);
|
|
return [left, right];
|
|
};
|
|
|
|
/**
|
|
* @param {any} loopWidths
|
|
* @param {any} msg
|
|
* @param {any} preMargin
|
|
* @param {any} postMargin
|
|
* @param {any} addLoopFn
|
|
*/
|
|
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
|
|
bounds.bumpVerticalPos(preMargin);
|
|
let heightAdjust = postMargin;
|
|
if (msg.id && msg.message && loopWidths[msg.id]) {
|
|
let loopWidth = loopWidths[msg.id].width;
|
|
let textConf = messageFont(conf);
|
|
msg.message = utils.wrapLabel(`[${msg.message}]`, loopWidth - 2 * conf.wrapPadding, textConf);
|
|
msg.width = loopWidth;
|
|
msg.wrap = true;
|
|
|
|
// const lines = common.splitBreaks(msg.message).length;
|
|
const textDims = utils.calculateTextDimensions(msg.message, textConf);
|
|
const totalOffset = Math.max(textDims.height, conf.labelBoxHeight);
|
|
heightAdjust = postMargin + totalOffset;
|
|
log.debug(`${totalOffset} - ${msg.message}`);
|
|
}
|
|
addLoopFn(msg);
|
|
bounds.bumpVerticalPos(heightAdjust);
|
|
}
|
|
|
|
/**
|
|
* Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
|
|
*
|
|
* @param {any} text
|
|
* @param {any} id
|
|
*/
|
|
export const draw = function (text, id) {
|
|
conf = configApi.getConfig().sequence;
|
|
const securityLevel = configApi.getConfig().securityLevel;
|
|
// Handle root and ocument for when rendering in sanbox mode
|
|
let sandboxElement;
|
|
if (securityLevel === 'sandbox') {
|
|
sandboxElement = select('#i' + id);
|
|
}
|
|
const root =
|
|
securityLevel === 'sandbox'
|
|
? select(sandboxElement.nodes()[0].contentDocument.body)
|
|
: select('body');
|
|
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
|
|
|
parser.yy.clear();
|
|
parser.yy.setWrap(conf.wrap);
|
|
parser.parse(text + '\n');
|
|
bounds.init();
|
|
log.debug(`C:${JSON.stringify(conf, null, 2)}`);
|
|
|
|
const diagram =
|
|
securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : select(`[id="${id}"]`);
|
|
|
|
// Fetch data from the parsing
|
|
const actors = parser.yy.getActors();
|
|
const actorKeys = parser.yy.getActorKeys();
|
|
const messages = parser.yy.getMessages();
|
|
const title = parser.yy.getDiagramTitle();
|
|
|
|
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages);
|
|
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor);
|
|
|
|
svgDraw.insertComputerIcon(diagram);
|
|
svgDraw.insertDatabaseIcon(diagram);
|
|
svgDraw.insertClockIcon(diagram);
|
|
|
|
drawActors(diagram, actors, actorKeys, 0, conf, messages);
|
|
const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor);
|
|
|
|
// The arrow head definition is attached to the svg once
|
|
svgDraw.insertArrowHead(diagram);
|
|
svgDraw.insertArrowCrossHead(diagram);
|
|
svgDraw.insertArrowFilledHead(diagram);
|
|
svgDraw.insertSequenceNumber(diagram);
|
|
|
|
/**
|
|
* @param {any} msg
|
|
* @param {any} verticalPos
|
|
*/
|
|
function activeEnd(msg, verticalPos) {
|
|
const activationData = bounds.endActivation(msg);
|
|
if (activationData.starty + 18 > verticalPos) {
|
|
activationData.starty = verticalPos - 6;
|
|
verticalPos += 12;
|
|
}
|
|
svgDraw.drawActivation(
|
|
diagram,
|
|
activationData,
|
|
verticalPos,
|
|
conf,
|
|
actorActivations(msg.from.actor).length
|
|
);
|
|
|
|
bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos);
|
|
}
|
|
|
|
// Draw the messages/signals
|
|
let sequenceIndex = 1;
|
|
let sequenceIndexStep = 1;
|
|
let messagesToDraw = Array();
|
|
messages.forEach(function (msg) {
|
|
let loopModel, noteModel, msgModel;
|
|
|
|
switch (msg.type) {
|
|
case parser.yy.LINETYPE.NOTE:
|
|
noteModel = msg.noteModel;
|
|
drawNote(diagram, noteModel);
|
|
break;
|
|
case parser.yy.LINETYPE.ACTIVE_START:
|
|
bounds.newActivation(msg, diagram, actors);
|
|
break;
|
|
case parser.yy.LINETYPE.ACTIVE_END:
|
|
activeEnd(msg, bounds.getVerticalPos());
|
|
break;
|
|
case parser.yy.LINETYPE.LOOP_START:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
(message) => bounds.newLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.LOOP_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawLoop(diagram, loopModel, 'loop', conf);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
bounds.models.addLoop(loopModel);
|
|
break;
|
|
case parser.yy.LINETYPE.RECT_START:
|
|
adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, (message) =>
|
|
bounds.newLoop(undefined, message.message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.RECT_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawBackgroundRect(diagram, loopModel);
|
|
bounds.models.addLoop(loopModel);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
break;
|
|
case parser.yy.LINETYPE.OPT_START:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
(message) => bounds.newLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.OPT_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawLoop(diagram, loopModel, 'opt', conf);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
bounds.models.addLoop(loopModel);
|
|
break;
|
|
case parser.yy.LINETYPE.ALT_START:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
(message) => bounds.newLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.ALT_ELSE:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
conf.boxMargin,
|
|
(message) => bounds.addSectionToLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.ALT_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawLoop(diagram, loopModel, 'alt', conf);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
bounds.models.addLoop(loopModel);
|
|
break;
|
|
case parser.yy.LINETYPE.PAR_START:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
(message) => bounds.newLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.PAR_AND:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
conf.boxMargin,
|
|
(message) => bounds.addSectionToLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.PAR_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawLoop(diagram, loopModel, 'par', conf);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
bounds.models.addLoop(loopModel);
|
|
break;
|
|
case parser.yy.LINETYPE.AUTONUMBER:
|
|
sequenceIndex = msg.message.start || sequenceIndex;
|
|
sequenceIndexStep = msg.message.step || sequenceIndexStep;
|
|
if (msg.message.visible) parser.yy.enableSequenceNumbers();
|
|
else parser.yy.disableSequenceNumbers();
|
|
break;
|
|
case parser.yy.LINETYPE.CRITICAL_START:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
(message) => bounds.newLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.CRITICAL_OPTION:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
conf.boxMargin,
|
|
(message) => bounds.addSectionToLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.CRITICAL_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawLoop(diagram, loopModel, 'critical', conf);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
bounds.models.addLoop(loopModel);
|
|
break;
|
|
case parser.yy.LINETYPE.BREAK_START:
|
|
adjustLoopHeightForWrap(
|
|
loopWidths,
|
|
msg,
|
|
conf.boxMargin,
|
|
conf.boxMargin + conf.boxTextMargin,
|
|
(message) => bounds.newLoop(message)
|
|
);
|
|
break;
|
|
case parser.yy.LINETYPE.BREAK_END:
|
|
loopModel = bounds.endLoop();
|
|
svgDraw.drawLoop(diagram, loopModel, 'break', conf);
|
|
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
|
|
bounds.models.addLoop(loopModel);
|
|
break;
|
|
default:
|
|
try {
|
|
// lastMsg = msg
|
|
msgModel = msg.msgModel;
|
|
msgModel.starty = bounds.getVerticalPos();
|
|
msgModel.sequenceIndex = sequenceIndex;
|
|
msgModel.sequenceVisible = parser.yy.showSequenceNumbers();
|
|
let lineStarty = boundMessage(diagram, msgModel);
|
|
messagesToDraw.push({ messageModel: msgModel, lineStarty: lineStarty });
|
|
bounds.models.addMessage(msgModel);
|
|
} catch (e) {
|
|
log.error('error while drawing message', e);
|
|
}
|
|
}
|
|
|
|
// Increment sequence counter if msg.type is a line (and not another event like activation or note, etc)
|
|
if (
|
|
[
|
|
parser.yy.LINETYPE.SOLID_OPEN,
|
|
parser.yy.LINETYPE.DOTTED_OPEN,
|
|
parser.yy.LINETYPE.SOLID,
|
|
parser.yy.LINETYPE.DOTTED,
|
|
parser.yy.LINETYPE.SOLID_CROSS,
|
|
parser.yy.LINETYPE.DOTTED_CROSS,
|
|
parser.yy.LINETYPE.SOLID_POINT,
|
|
parser.yy.LINETYPE.DOTTED_POINT,
|
|
].includes(msg.type)
|
|
) {
|
|
sequenceIndex = sequenceIndex + sequenceIndexStep;
|
|
}
|
|
});
|
|
|
|
messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStarty));
|
|
|
|
if (conf.mirrorActors) {
|
|
// Draw actors below diagram
|
|
bounds.bumpVerticalPos(conf.boxMargin * 2);
|
|
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages);
|
|
bounds.bumpVerticalPos(conf.boxMargin);
|
|
fixLifeLineHeights(diagram, bounds.getVerticalPos());
|
|
}
|
|
|
|
// only draw popups for the top row of actors.
|
|
var requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc);
|
|
|
|
const { bounds: box } = bounds.getBounds();
|
|
|
|
// Adjust line height of actor lines now that the height of the diagram is known
|
|
log.debug('For line height fix Querying: #' + id + ' .actor-line');
|
|
const actorLines = selectAll('#' + id + ' .actor-line');
|
|
actorLines.attr('y2', box.stopy);
|
|
|
|
// Make sure the height of the diagram supports long menus.
|
|
let boxHeight = box.stopy - box.starty;
|
|
if (boxHeight < requiredBoxSize.maxHeight) {
|
|
boxHeight = requiredBoxSize.maxHeight;
|
|
}
|
|
|
|
let height = boxHeight + 2 * conf.diagramMarginY;
|
|
if (conf.mirrorActors) {
|
|
height = height - conf.boxMargin + conf.bottomMarginAdj;
|
|
}
|
|
|
|
// Make sure the width of the diagram supports wide menus.
|
|
let boxWidth = box.stopx - box.startx;
|
|
if (boxWidth < requiredBoxSize.maxWidth) {
|
|
boxWidth = requiredBoxSize.maxWidth;
|
|
}
|
|
const width = boxWidth + 2 * conf.diagramMarginX;
|
|
|
|
if (title) {
|
|
diagram
|
|
.append('text')
|
|
.text(title)
|
|
.attr('x', (box.stopx - box.startx) / 2 - 2 * conf.diagramMarginX)
|
|
.attr('y', -25);
|
|
}
|
|
|
|
configureSvgSize(diagram, height, width, conf.useMaxWidth);
|
|
|
|
const extraVertForTitle = title ? 40 : 0;
|
|
diagram.attr(
|
|
'viewBox',
|
|
box.startx -
|
|
conf.diagramMarginX +
|
|
' -' +
|
|
(conf.diagramMarginY + extraVertForTitle) +
|
|
' ' +
|
|
width +
|
|
' ' +
|
|
(height + extraVertForTitle)
|
|
);
|
|
|
|
addSVGAccessibilityFields(parser.yy, diagram, id);
|
|
log.debug(`models:`, bounds.models);
|
|
};
|
|
|
|
/**
|
|
* Retrieves the max message width of each actor, supports signals (messages, loops) and notes.
|
|
*
|
|
* It will enumerate each given message, and will determine its text width, in relation to the actor
|
|
* it originates from, and destined to.
|
|
*
|
|
* @param {any} actors - The actors map
|
|
* @param {Array} messages - A list of message objects to iterate
|
|
* @returns {any}
|
|
*/
|
|
const getMaxMessageWidthPerActor = function (actors, messages) {
|
|
const maxMessageWidthPerActor = {};
|
|
|
|
messages.forEach(function (msg) {
|
|
if (actors[msg.to] && actors[msg.from]) {
|
|
const actor = actors[msg.to];
|
|
|
|
// If this is the first actor, and the message is left of it, no need to calculate the margin
|
|
if (msg.placement === parser.yy.PLACEMENT.LEFTOF && !actor.prevActor) {
|
|
return;
|
|
}
|
|
|
|
// If this is the last actor, and the message is right of it, no need to calculate the margin
|
|
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF && !actor.nextActor) {
|
|
return;
|
|
}
|
|
|
|
const isNote = msg.placement !== undefined;
|
|
const isMessage = !isNote;
|
|
|
|
const textFont = isNote ? noteFont(conf) : messageFont(conf);
|
|
let wrappedMessage = msg.wrap
|
|
? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont)
|
|
: msg.message;
|
|
const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont);
|
|
const messageWidth = messageDimensions.width + 2 * conf.wrapPadding;
|
|
|
|
/*
|
|
* The following scenarios should be supported:
|
|
*
|
|
* - There's a message (non-note) between fromActor and toActor
|
|
* - If fromActor is on the right and toActor is on the left, we should
|
|
* define the toActor's margin
|
|
* - If fromActor is on the left and toActor is on the right, we should
|
|
* define the fromActor's margin
|
|
* - There's a note, in which case fromActor == toActor
|
|
* - If the note is to the left of the actor, we should define the previous actor
|
|
* margin
|
|
* - If the note is on the actor, we should define both the previous and next actor
|
|
* margins, each being the half of the note size
|
|
* - If the note is on the right of the actor, we should define the current actor
|
|
* margin
|
|
*/
|
|
if (isMessage && msg.from === actor.nextActor) {
|
|
maxMessageWidthPerActor[msg.to] = Math.max(
|
|
maxMessageWidthPerActor[msg.to] || 0,
|
|
messageWidth
|
|
);
|
|
} else if (isMessage && msg.from === actor.prevActor) {
|
|
maxMessageWidthPerActor[msg.from] = Math.max(
|
|
maxMessageWidthPerActor[msg.from] || 0,
|
|
messageWidth
|
|
);
|
|
} else if (isMessage && msg.from === msg.to) {
|
|
maxMessageWidthPerActor[msg.from] = Math.max(
|
|
maxMessageWidthPerActor[msg.from] || 0,
|
|
messageWidth / 2
|
|
);
|
|
|
|
maxMessageWidthPerActor[msg.to] = Math.max(
|
|
maxMessageWidthPerActor[msg.to] || 0,
|
|
messageWidth / 2
|
|
);
|
|
} else if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
|
|
maxMessageWidthPerActor[msg.from] = Math.max(
|
|
maxMessageWidthPerActor[msg.from] || 0,
|
|
messageWidth
|
|
);
|
|
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
|
|
maxMessageWidthPerActor[actor.prevActor] = Math.max(
|
|
maxMessageWidthPerActor[actor.prevActor] || 0,
|
|
messageWidth
|
|
);
|
|
} else if (msg.placement === parser.yy.PLACEMENT.OVER) {
|
|
if (actor.prevActor) {
|
|
maxMessageWidthPerActor[actor.prevActor] = Math.max(
|
|
maxMessageWidthPerActor[actor.prevActor] || 0,
|
|
messageWidth / 2
|
|
);
|
|
}
|
|
|
|
if (actor.nextActor) {
|
|
maxMessageWidthPerActor[msg.from] = Math.max(
|
|
maxMessageWidthPerActor[msg.from] || 0,
|
|
messageWidth / 2
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor);
|
|
return maxMessageWidthPerActor;
|
|
};
|
|
|
|
const getRequiredPopupWidth = function (actor) {
|
|
let requiredPopupWidth = 0;
|
|
const textFont = actorFont(conf);
|
|
for (let key in actor.links) {
|
|
let labelDimensions = utils.calculateTextDimensions(key, textFont);
|
|
let labelWidth = labelDimensions.width + 2 * conf.wrapPadding + 2 * conf.boxMargin;
|
|
if (requiredPopupWidth < labelWidth) {
|
|
requiredPopupWidth = labelWidth;
|
|
}
|
|
}
|
|
|
|
return requiredPopupWidth;
|
|
};
|
|
|
|
/**
|
|
* This will calculate the optimal margin for each given actor, for a given actor->messageWidth map.
|
|
*
|
|
* An actor's margin is determined by the width of the actor, the width of the largest message that
|
|
* originates from it, and the configured conf.actorMargin.
|
|
*
|
|
* @param {any} actors - The actors map to calculate margins for
|
|
* @param {any} actorToMessageWidth - A map of actor key -> max message width it holds
|
|
*/
|
|
const calculateActorMargins = function (actors, actorToMessageWidth) {
|
|
let maxHeight = 0;
|
|
Object.keys(actors).forEach((prop) => {
|
|
const actor = actors[prop];
|
|
if (actor.wrap) {
|
|
actor.description = utils.wrapLabel(
|
|
actor.description,
|
|
conf.width - 2 * conf.wrapPadding,
|
|
actorFont(conf)
|
|
);
|
|
}
|
|
const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf));
|
|
actor.width = actor.wrap
|
|
? conf.width
|
|
: Math.max(conf.width, actDims.width + 2 * conf.wrapPadding);
|
|
|
|
actor.height = actor.wrap ? Math.max(actDims.height, conf.height) : conf.height;
|
|
maxHeight = Math.max(maxHeight, actor.height);
|
|
});
|
|
|
|
for (let actorKey in actorToMessageWidth) {
|
|
const actor = actors[actorKey];
|
|
|
|
if (!actor) {
|
|
continue;
|
|
}
|
|
|
|
const nextActor = actors[actor.nextActor];
|
|
|
|
// No need to space out an actor that doesn't have a next link
|
|
if (!nextActor) {
|
|
continue;
|
|
}
|
|
|
|
const messageWidth = actorToMessageWidth[actorKey];
|
|
const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2;
|
|
|
|
actor.margin = Math.max(actorWidth, conf.actorMargin);
|
|
}
|
|
|
|
return Math.max(maxHeight, conf.height);
|
|
};
|
|
|
|
const buildNoteModel = function (msg, actors) {
|
|
let startx = actors[msg.from].x;
|
|
let stopx = actors[msg.to].x;
|
|
let shouldWrap = msg.wrap && msg.message;
|
|
|
|
let textDimensions = utils.calculateTextDimensions(
|
|
shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message,
|
|
noteFont(conf)
|
|
);
|
|
let noteModel = {
|
|
width: shouldWrap
|
|
? conf.width
|
|
: Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin),
|
|
height: 0,
|
|
startx: actors[msg.from].x,
|
|
stopx: 0,
|
|
starty: 0,
|
|
stopy: 0,
|
|
message: msg.message,
|
|
};
|
|
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
|
|
noteModel.width = shouldWrap
|
|
? Math.max(conf.width, textDimensions.width)
|
|
: Math.max(
|
|
actors[msg.from].width / 2 + actors[msg.to].width / 2,
|
|
textDimensions.width + 2 * conf.noteMargin
|
|
);
|
|
noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2;
|
|
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
|
|
noteModel.width = shouldWrap
|
|
? Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin)
|
|
: Math.max(
|
|
actors[msg.from].width / 2 + actors[msg.to].width / 2,
|
|
textDimensions.width + 2 * conf.noteMargin
|
|
);
|
|
noteModel.startx = startx - noteModel.width + (actors[msg.from].width - conf.actorMargin) / 2;
|
|
} else if (msg.to === msg.from) {
|
|
textDimensions = utils.calculateTextDimensions(
|
|
shouldWrap
|
|
? utils.wrapLabel(msg.message, Math.max(conf.width, actors[msg.from].width), noteFont(conf))
|
|
: msg.message,
|
|
noteFont(conf)
|
|
);
|
|
noteModel.width = shouldWrap
|
|
? Math.max(conf.width, actors[msg.from].width)
|
|
: Math.max(actors[msg.from].width, conf.width, textDimensions.width + 2 * conf.noteMargin);
|
|
noteModel.startx = startx + (actors[msg.from].width - noteModel.width) / 2;
|
|
} else {
|
|
noteModel.width =
|
|
Math.abs(startx + actors[msg.from].width / 2 - (stopx + actors[msg.to].width / 2)) +
|
|
conf.actorMargin;
|
|
noteModel.startx =
|
|
startx < stopx
|
|
? startx + actors[msg.from].width / 2 - conf.actorMargin / 2
|
|
: stopx + actors[msg.to].width / 2 - conf.actorMargin / 2;
|
|
}
|
|
if (shouldWrap) {
|
|
noteModel.message = utils.wrapLabel(
|
|
msg.message,
|
|
noteModel.width - 2 * conf.wrapPadding,
|
|
noteFont(conf)
|
|
);
|
|
}
|
|
log.debug(
|
|
`NM:[${noteModel.startx},${noteModel.stopx},${noteModel.starty},${noteModel.stopy}:${noteModel.width},${noteModel.height}=${msg.message}]`
|
|
);
|
|
return noteModel;
|
|
};
|
|
|
|
const buildMessageModel = function (msg, actors) {
|
|
let process = false;
|
|
if (
|
|
[
|
|
parser.yy.LINETYPE.SOLID_OPEN,
|
|
parser.yy.LINETYPE.DOTTED_OPEN,
|
|
parser.yy.LINETYPE.SOLID,
|
|
parser.yy.LINETYPE.DOTTED,
|
|
parser.yy.LINETYPE.SOLID_CROSS,
|
|
parser.yy.LINETYPE.DOTTED_CROSS,
|
|
parser.yy.LINETYPE.SOLID_POINT,
|
|
parser.yy.LINETYPE.DOTTED_POINT,
|
|
].includes(msg.type)
|
|
) {
|
|
process = true;
|
|
}
|
|
if (!process) {
|
|
return {};
|
|
}
|
|
const fromBounds = activationBounds(msg.from, actors);
|
|
const toBounds = activationBounds(msg.to, actors);
|
|
const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
|
|
const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
|
|
const allBounds = fromBounds.concat(toBounds);
|
|
const boundedWidth = Math.abs(toBounds[toIdx] - fromBounds[fromIdx]);
|
|
if (msg.wrap && msg.message) {
|
|
msg.message = utils.wrapLabel(
|
|
msg.message,
|
|
Math.max(boundedWidth + 2 * conf.wrapPadding, conf.width),
|
|
messageFont(conf)
|
|
);
|
|
}
|
|
const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf));
|
|
|
|
return {
|
|
width: Math.max(
|
|
msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding,
|
|
boundedWidth + 2 * conf.wrapPadding,
|
|
conf.width
|
|
),
|
|
height: 0,
|
|
startx: fromBounds[fromIdx],
|
|
stopx: toBounds[toIdx],
|
|
starty: 0,
|
|
stopy: 0,
|
|
message: msg.message,
|
|
type: msg.type,
|
|
wrap: msg.wrap,
|
|
fromBounds: Math.min.apply(null, allBounds),
|
|
toBounds: Math.max.apply(null, allBounds),
|
|
};
|
|
};
|
|
|
|
const calculateLoopBounds = function (messages, actors) {
|
|
const loops = {};
|
|
const stack = [];
|
|
let current, noteModel, msgModel;
|
|
|
|
messages.forEach(function (msg) {
|
|
msg.id = utils.random({ length: 10 });
|
|
switch (msg.type) {
|
|
case parser.yy.LINETYPE.LOOP_START:
|
|
case parser.yy.LINETYPE.ALT_START:
|
|
case parser.yy.LINETYPE.OPT_START:
|
|
case parser.yy.LINETYPE.PAR_START:
|
|
case parser.yy.LINETYPE.CRITICAL_START:
|
|
case parser.yy.LINETYPE.BREAK_START:
|
|
stack.push({
|
|
id: msg.id,
|
|
msg: msg.message,
|
|
from: Number.MAX_SAFE_INTEGER,
|
|
to: Number.MIN_SAFE_INTEGER,
|
|
width: 0,
|
|
});
|
|
break;
|
|
case parser.yy.LINETYPE.ALT_ELSE:
|
|
case parser.yy.LINETYPE.PAR_AND:
|
|
case parser.yy.LINETYPE.CRITICAL_OPTION:
|
|
if (msg.message) {
|
|
current = stack.pop();
|
|
loops[current.id] = current;
|
|
loops[msg.id] = current;
|
|
stack.push(current);
|
|
}
|
|
break;
|
|
case parser.yy.LINETYPE.LOOP_END:
|
|
case parser.yy.LINETYPE.ALT_END:
|
|
case parser.yy.LINETYPE.OPT_END:
|
|
case parser.yy.LINETYPE.PAR_END:
|
|
case parser.yy.LINETYPE.CRITICAL_END:
|
|
case parser.yy.LINETYPE.BREAK_END:
|
|
current = stack.pop();
|
|
loops[current.id] = current;
|
|
break;
|
|
case parser.yy.LINETYPE.ACTIVE_START:
|
|
{
|
|
const actorRect = actors[msg.from ? msg.from.actor : msg.to.actor];
|
|
const stackedSize = actorActivations(msg.from ? msg.from.actor : msg.to.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;
|
|
}
|
|
const isNote = msg.placement !== undefined;
|
|
if (isNote) {
|
|
noteModel = buildNoteModel(msg, actors);
|
|
msg.noteModel = noteModel;
|
|
stack.forEach((stk) => {
|
|
current = stk;
|
|
current.from = Math.min(current.from, noteModel.startx);
|
|
current.to = Math.max(current.to, noteModel.startx + noteModel.width);
|
|
current.width =
|
|
Math.max(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth;
|
|
});
|
|
} else {
|
|
msgModel = buildMessageModel(msg, actors);
|
|
msg.msgModel = msgModel;
|
|
if (msgModel.startx && msgModel.stopx && stack.length > 0) {
|
|
stack.forEach((stk) => {
|
|
current = stk;
|
|
if (msgModel.startx === msgModel.stopx) {
|
|
let from = actors[msg.from];
|
|
let to = actors[msg.to];
|
|
current.from = Math.min(
|
|
from.x - msgModel.width / 2,
|
|
from.x - from.width / 2,
|
|
current.from
|
|
);
|
|
current.to = Math.max(to.x + msgModel.width / 2, to.x + from.width / 2, current.to);
|
|
current.width =
|
|
Math.max(current.width, Math.abs(current.to - current.from)) - conf.labelBoxWidth;
|
|
} else {
|
|
current.from = Math.min(msgModel.startx, current.from);
|
|
current.to = Math.max(msgModel.stopx, current.to);
|
|
current.width = Math.max(current.width, msgModel.width) - conf.labelBoxWidth;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
bounds.activations = [];
|
|
log.debug('Loop type widths:', loops);
|
|
return loops;
|
|
};
|
|
|
|
export default {
|
|
bounds,
|
|
drawActors,
|
|
drawActorsPopup,
|
|
setConf,
|
|
draw,
|
|
};
|