// @ts-nocheck TODO: fix file import { select } from 'd3'; import svgDraw, { drawKatex, ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js'; import { log } from '../../logger.js'; import common, { calculateMathMLDimensions, hasKatex } from '../common/common.js'; import { getUrl } from '../common/common.js'; import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import assignWithDepth from '../../assignWithDepth.js'; import utils from '../../utils.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import type { Diagram } from '../../Diagram.js'; import { PARTICIPANT_TYPE } from './sequenceDb.js'; 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.boxes = []; this.loops = []; this.messages = []; this.notes = []; }, addBox: function (boxModel) { this.boxes.push(boxModel); }, 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: [], boxes: [], 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(getConfig()); }, updateVal: function (obj, key, val, fun) { if (obj[key] === undefined) { obj[key] = val; } else { obj[key] = fun(val, obj[key]); } }, updateBounds: function (startx, starty, stopx, stopy) { // eslint-disable-next-line @typescript-eslint/no-this-alias const _self = this; let cnt = 0; /** @param type - Either `activation` or `undefined` */ function updateFn(type?: 'activation') { 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 = common.getMin(startx, stopx); const _stopx = common.getMax(startx, stopx); const _starty = common.getMin(starty, stopy); const _stopy = common.getMax(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.get(message.from); const stackedSize = actorActivations(message.from).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, 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); 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(); }, isLoopOverlap: function () { return this.sequenceItems.length ? this.sequenceItems[this.sequenceItems.length - 1].overlap : false; }, 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); }, saveVerticalPos: function () { if (this.isLoopOverlap()) { this.savedVerticalPos = this.verticalPos; } }, resetVerticalPos: function () { if (this.isLoopOverlap()) { this.verticalPos = this.savedVerticalPos; } }, bumpVerticalPos: function (bump) { this.verticalPos = this.verticalPos + bump; this.data.stopy = common.getMax(this.data.stopy, this.verticalPos); }, getVerticalPos: function () { return this.verticalPos; }, getBounds: function () { return { bounds: this.data, models: this.models }; }, }; /** Options for drawing a note in {@link drawNote} */ interface NoteModel { /** x axis start position */ startx: number; /** y axis position */ starty: number; /** the message to be shown */ message: string; /** Set this with a custom width to override the default configured width. */ width: number; } /** * Draws a note in the diagram with the attached line * * @param elem - The diagram to draw to. * @param noteModel - Note model options. */ const drawNote = async function (elem: any, noteModel: NoteModel) { bounds.bumpVerticalPos(conf.boxMargin); noteModel.height = conf.boxMargin; noteModel.starty = bounds.getVerticalPos(); const rect = svgDrawCommon.getNoteRect(); rect.x = noteModel.startx; rect.y = noteModel.starty; rect.width = noteModel.width || conf.width; rect.class = 'note'; const g = elem.append('g'); const rectElem = svgDraw.drawRect(g, rect); const textObj = svgDrawCommon.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 = 'center'; const textElem = hasKatex(textObj.text) ? await drawKatex(g, textObj) : drawText(g, textObj); const 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 _diagram - The parent of the message element. * @param msgModel - The model containing fields describing a message * @returns `lineStartY` - The Y coordinate at which the message line starts */ async function boundMessage(_diagram, msgModel): Promise { bounds.bumpVerticalPos(10); const { startx, stopx, message } = msgModel; const lines = common.splitBreaks(message).length; const isKatexMsg = hasKatex(message); const textDims = isKatexMsg ? await calculateMathMLDimensions(message, getConfig()) : utils.calculateTextDimensions(message, messageFont(conf)); if (!isKatexMsg) { const lineHeight = textDims.height / lines; msgModel.height += lineHeight; bounds.bumpVerticalPos(lineHeight); } let lineStartY; let totalOffset = textDims.height - 10; const textWidth = textDims.width; if (startx === stopx) { lineStartY = bounds.getVerticalPos() + totalOffset; if (!conf.rightAngles) { totalOffset += conf.boxMargin; lineStartY = bounds.getVerticalPos() + totalOffset; } totalOffset += 30; const dx = common.getMax(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 diagram - The parent of the message element * @param msgModel - The model containing fields describing a message * @param lineStartY - The Y coordinate at which the message line starts * @param diagObj - The diagram object. */ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textObj = svgDrawCommon.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 = 'center'; textObj.textMargin = conf.wrapPadding; textObj.tspan = false; if (hasKatex(textObj.text)) { await drawKatex(diagram, textObj, { startx, stopx, starty: lineStartY }); } else { drawText(diagram, textObj); } const textWidth = textDims.width; let line; if (startx === stopx) { if (conf.rightAngles) { line = diagram .append('path') .attr( 'd', `M ${startx},${lineStartY} H ${ startx + common.getMax(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 === diagObj.db.LINETYPE.DOTTED || type === diagObj.db.LINETYPE.DOTTED_CROSS || type === diagObj.db.LINETYPE.DOTTED_POINT || type === diagObj.db.LINETYPE.DOTTED_OPEN || type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED ) { line.style('stroke-dasharray', '3, 3'); line.attr('class', 'messageLine1'); } else { line.attr('class', 'messageLine0'); } let url = ''; if (conf.arrowMarkerAbsolute) { url = getUrl(true); } line.attr('stroke-width', 2); line.attr('stroke', 'none'); // handled by theme/css anyway line.style('fill', 'none'); // remove any fill colour if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) { line.attr('marker-end', 'url(' + url + '#arrowhead)'); } if ( type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID || type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED ) { line.attr('marker-start', 'url(' + url + '#arrowhead)'); line.attr('marker-end', 'url(' + url + '#arrowhead)'); } if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) { line.attr('marker-end', 'url(' + url + '#filled-head)'); } if (type === diagObj.db.LINETYPE.SOLID_CROSS || type === diagObj.db.LINETYPE.DOTTED_CROSS) { line.attr('marker-end', 'url(' + url + '#crosshead)'); } // add node number if (sequenceVisible || conf.showSequenceNumbers) { const isBidirectional = type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID || type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED; if (isBidirectional) { const SEQUENCE_NUMBER_RADIUS = 6; if (startx < stopx) { line.attr('x1', startx + 2 * SEQUENCE_NUMBER_RADIUS); } else { line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS); } } diagram .append('line') .attr('x1', startx) .attr('y1', lineStartY) .attr('x2', startx) .attr('y2', lineStartY) .attr('stroke-width', 0) .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('class', 'sequenceNumber') .text(sequenceIndex); } }; const addActorRenderingData = function ( diagram, actors, createdActors: Map, actorKeys, verticalPos, messages, isFooter ) { let prevWidth = 0; let prevMargin = 0; let prevBox = undefined; let maxHeight = 0; for (const actorKey of actorKeys) { const actor = actors.get(actorKey); const box = actor.box; // end of box if (prevBox && prevBox != box) { if (!isFooter) { bounds.models.addBox(prevBox); } prevMargin += conf.boxMargin + prevBox.margin; } // new box if (box && box != prevBox) { if (!isFooter) { box.x = prevWidth + prevMargin; box.y = verticalPos; } prevMargin += box.margin; } // Add some rendering data to the object actor.width = actor.width || conf.width; actor.height = common.getMax(actor.height || conf.height, conf.height); actor.margin = actor.margin || conf.actorMargin; maxHeight = common.getMax(maxHeight, actor.height); // if the actor is created by a message, widen margin if (createdActors.get(actor.name)) { prevMargin += actor.width / 2; } actor.x = prevWidth + prevMargin; actor.starty = bounds.getVerticalPos(); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height); prevWidth += actor.width + prevMargin; if (actor.box) { actor.box.width = prevWidth + box.margin - actor.box.x; } prevMargin = actor.margin; prevBox = actor.box; bounds.models.addActor(actor); } // end of box if (prevBox && !isFooter) { bounds.models.addBox(prevBox); } // Add a margin between the actor boxes and the first arrow bounds.bumpVerticalPos(maxHeight); }; export const drawActors = async function (diagram, actors, actorKeys, isFooter) { if (!isFooter) { for (const actorKey of actorKeys) { const actor = actors.get(actorKey); // Draw the box with the attached line await svgDraw.drawActor(diagram, actor, conf, false); } } else { let maxHeight = 0; bounds.bumpVerticalPos(conf.boxMargin * 2); for (const actorKey of actorKeys) { const actor = actors.get(actorKey); if (!actor.stopy) { actor.stopy = bounds.getVerticalPos(); } const height = await svgDraw.drawActor(diagram, actor, conf, true); maxHeight = common.getMax(maxHeight, height); } bounds.bumpVerticalPos(maxHeight + conf.boxMargin); } }; export const drawActorsPopup = function (diagram, actors, actorKeys, doc) { let maxHeight = 0; let maxWidth = 0; for (const actorKey of actorKeys) { const actor = actors.get(actorKey); const minMenuWidth = getRequiredPopupWidth(actor); const 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.get(actor); const activations = actorActivations(actor); const left = activations.reduce( function (acc, activation) { return common.getMin(acc, activation.startx); }, actorObj.x + actorObj.width / 2 - 1 ); const right = activations.reduce( function (acc, activation) { return common.getMax(acc, activation.stopx); }, actorObj.x + actorObj.width / 2 + 1 ); return [left, right]; }; function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) { bounds.bumpVerticalPos(preMargin); let heightAdjust = postMargin; if (msg.id && msg.message && loopWidths[msg.id]) { const loopWidth = loopWidths[msg.id].width; const 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 = common.getMax(textDims.height, conf.labelBoxHeight); heightAdjust = postMargin + totalOffset; log.debug(`${totalOffset} - ${msg.message}`); } addLoopFn(msg); bounds.bumpVerticalPos(heightAdjust); } /** * Adjust the msgModel and the actor for the rendering in case the latter is created or destroyed by the msg * @param msg - the potentially creating or destroying message * @param msgModel - the model associated with the message * @param lineStartY - the y position of the message line * @param index - the index of the current actor under consideration * @param actors - the array of all actors * @param createdActors - the array of actors created in the diagram * @param destroyedActors - the array of actors destroyed in the diagram */ function adjustCreatedDestroyedData( msg, msgModel, lineStartY, index, actors, createdActors, destroyedActors ) { function receiverAdjustment(actor, adjustment) { if (actor.x < actors.get(msg.from).x) { bounds.insert( msgModel.stopx - adjustment, msgModel.starty, msgModel.startx, msgModel.stopy + actor.height / 2 + conf.noteMargin ); msgModel.stopx = msgModel.stopx + adjustment; } else { bounds.insert( msgModel.startx, msgModel.starty, msgModel.stopx + adjustment, msgModel.stopy + actor.height / 2 + conf.noteMargin ); msgModel.stopx = msgModel.stopx - adjustment; } } function senderAdjustment(actor, adjustment) { if (actor.x < actors.get(msg.to).x) { bounds.insert( msgModel.startx - adjustment, msgModel.starty, msgModel.stopx, msgModel.stopy + actor.height / 2 + conf.noteMargin ); msgModel.startx = msgModel.startx + adjustment; } else { bounds.insert( msgModel.stopx, msgModel.starty, msgModel.startx + adjustment, msgModel.stopy + actor.height / 2 + conf.noteMargin ); msgModel.startx = msgModel.startx - adjustment; } } const actorArray = [ PARTICIPANT_TYPE.ACTOR, PARTICIPANT_TYPE.CONTROL, PARTICIPANT_TYPE.ENTITY, PARTICIPANT_TYPE.DATABASE, ]; // if it is a create message if (createdActors.get(msg.to) == index) { const actor = actors.get(msg.to); const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; receiverAdjustment(actor, adjustment); actor.starty = lineStartY - actor.height / 2; bounds.bumpVerticalPos(actor.height / 2); } // if it is a destroy sender message else if (destroyedActors.get(msg.from) == index) { const actor = actors.get(msg.from); if (conf.mirrorActors) { const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2; senderAdjustment(actor, adjustment); } actor.stopy = lineStartY - actor.height / 2; bounds.bumpVerticalPos(actor.height / 2); } // if it is a destroy receiver message else if (destroyedActors.get(msg.to) == index) { const actor = actors.get(msg.to); if (conf.mirrorActors) { const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; receiverAdjustment(actor, adjustment); } actor.stopy = lineStartY - actor.height / 2; bounds.bumpVerticalPos(actor.height / 2); } } /** * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text. * * @param _text - The text of the diagram * @param id - The id of the diagram which will be used as a DOM element id¨ * @param _version - Mermaid version from package.json * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram */ export const draw = async function (_text: string, id: string, _version: string, diagObj: Diagram) { const { securityLevel, sequence } = getConfig(); conf = sequence; // Handle root and Document for when rendering in sandbox 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; bounds.init(); log.debug(diagObj.db); const diagram = securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : select(`[id="${id}"]`); // Fetch data from the parsing const actors = diagObj.db.getActors(); const createdActors = diagObj.db.getCreatedActors(); const destroyedActors = diagObj.db.getDestroyedActors(); const boxes = diagObj.db.getBoxes(); let actorKeys = diagObj.db.getActorKeys(); const messages = diagObj.db.getMessages(); const title = diagObj.db.getDiagramTitle(); const hasBoxes = diagObj.db.hasAtLeastOneBox(); const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle(); const maxMessageWidthPerActor = await getMaxMessageWidthPerActor(actors, messages, diagObj); conf.height = await calculateActorMargins(actors, maxMessageWidthPerActor, boxes); svgDraw.insertComputerIcon(diagram); svgDraw.insertDatabaseIcon(diagram); svgDraw.insertClockIcon(diagram); if (hasBoxes) { bounds.bumpVerticalPos(conf.boxMargin); if (hasBoxTitles) { bounds.bumpVerticalPos(boxes[0].textMaxHeight); } } if (conf.hideUnusedParticipants === true) { const newActors = new Set(); messages.forEach((message) => { newActors.add(message.from); newActors.add(message.to); }); actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey)); } addActorRenderingData(diagram, actors, createdActors, actorKeys, 0, messages, false); const loopWidths = await calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj); // The arrow head definition is attached to the svg once svgDraw.insertArrowHead(diagram); svgDraw.insertArrowCrossHead(diagram); svgDraw.insertArrowFilledHead(diagram); svgDraw.insertSequenceNumber(diagram); /** * @param msg - The message to draw. * @param verticalPos - The vertical position of the message. */ function activeEnd(msg: any, verticalPos: number) { 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).length ); bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos); } // Draw the messages/signals let sequenceIndex = 1; let sequenceIndexStep = 1; const messagesToDraw = []; const backgrounds = []; let index = 0; for (const msg of messages) { let loopModel, noteModel, msgModel; switch (msg.type) { case diagObj.db.LINETYPE.NOTE: bounds.resetVerticalPos(); noteModel = msg.noteModel; await drawNote(diagram, noteModel); break; case diagObj.db.LINETYPE.ACTIVE_START: bounds.newActivation(msg, diagram, actors); break; case diagObj.db.LINETYPE.ACTIVE_END: activeEnd(msg, bounds.getVerticalPos()); break; case diagObj.db.LINETYPE.LOOP_START: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin, conf.boxMargin + conf.boxTextMargin, (message) => bounds.newLoop(message) ); break; case diagObj.db.LINETYPE.LOOP_END: loopModel = bounds.endLoop(); await svgDraw.drawLoop(diagram, loopModel, 'loop', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; case diagObj.db.LINETYPE.RECT_START: adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, (message) => bounds.newLoop(undefined, message.message) ); break; case diagObj.db.LINETYPE.RECT_END: loopModel = bounds.endLoop(); backgrounds.push(loopModel); bounds.models.addLoop(loopModel); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; case diagObj.db.LINETYPE.OPT_START: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin, conf.boxMargin + conf.boxTextMargin, (message) => bounds.newLoop(message) ); break; case diagObj.db.LINETYPE.OPT_END: loopModel = bounds.endLoop(); await svgDraw.drawLoop(diagram, loopModel, 'opt', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; case diagObj.db.LINETYPE.ALT_START: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin, conf.boxMargin + conf.boxTextMargin, (message) => bounds.newLoop(message) ); break; case diagObj.db.LINETYPE.ALT_ELSE: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin + conf.boxTextMargin, conf.boxMargin, (message) => bounds.addSectionToLoop(message) ); break; case diagObj.db.LINETYPE.ALT_END: loopModel = bounds.endLoop(); await svgDraw.drawLoop(diagram, loopModel, 'alt', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; case diagObj.db.LINETYPE.PAR_START: case diagObj.db.LINETYPE.PAR_OVER_START: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin, conf.boxMargin + conf.boxTextMargin, (message) => bounds.newLoop(message) ); bounds.saveVerticalPos(); break; case diagObj.db.LINETYPE.PAR_AND: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin + conf.boxTextMargin, conf.boxMargin, (message) => bounds.addSectionToLoop(message) ); break; case diagObj.db.LINETYPE.PAR_END: loopModel = bounds.endLoop(); await svgDraw.drawLoop(diagram, loopModel, 'par', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; case diagObj.db.LINETYPE.AUTONUMBER: sequenceIndex = msg.message.start || sequenceIndex; sequenceIndexStep = msg.message.step || sequenceIndexStep; if (msg.message.visible) { diagObj.db.enableSequenceNumbers(); } else { diagObj.db.disableSequenceNumbers(); } break; case diagObj.db.LINETYPE.CRITICAL_START: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin, conf.boxMargin + conf.boxTextMargin, (message) => bounds.newLoop(message) ); break; case diagObj.db.LINETYPE.CRITICAL_OPTION: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin + conf.boxTextMargin, conf.boxMargin, (message) => bounds.addSectionToLoop(message) ); break; case diagObj.db.LINETYPE.CRITICAL_END: loopModel = bounds.endLoop(); await svgDraw.drawLoop(diagram, loopModel, 'critical', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; case diagObj.db.LINETYPE.BREAK_START: adjustLoopHeightForWrap( loopWidths, msg, conf.boxMargin, conf.boxMargin + conf.boxTextMargin, (message) => bounds.newLoop(message) ); break; case diagObj.db.LINETYPE.BREAK_END: loopModel = bounds.endLoop(); await svgDraw.drawLoop(diagram, loopModel, 'break', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; default: try { msgModel = msg.msgModel; msgModel.starty = bounds.getVerticalPos(); msgModel.sequenceIndex = sequenceIndex; msgModel.sequenceVisible = diagObj.db.showSequenceNumbers(); const lineStartY = await boundMessage(diagram, msgModel); adjustCreatedDestroyedData( msg, msgModel, lineStartY, index, actors, createdActors, destroyedActors ); 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 ( [ diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN, diagObj.db.LINETYPE.SOLID, diagObj.db.LINETYPE.DOTTED, diagObj.db.LINETYPE.SOLID_CROSS, diagObj.db.LINETYPE.DOTTED_CROSS, diagObj.db.LINETYPE.SOLID_POINT, diagObj.db.LINETYPE.DOTTED_POINT, diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED, ].includes(msg.type) ) { sequenceIndex = sequenceIndex + sequenceIndexStep; } index++; } log.debug('createdActors', createdActors); log.debug('destroyedActors', destroyedActors); await drawActors(diagram, actors, actorKeys, false); for (const e of messagesToDraw) { await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj); } if (conf.mirrorActors) { await drawActors(diagram, actors, actorKeys, true); } backgrounds.forEach((e) => svgDraw.drawBackgroundRect(diagram, e)); fixLifeLineHeights(diagram, actors, actorKeys, conf); for (const box of bounds.models.boxes) { box.height = bounds.getVerticalPos() - box.y; bounds.insert(box.x, box.y, box.x + box.width, box.height); const boxPadding = conf.boxMargin * 2; box.startx = box.x - boxPadding; box.starty = box.y - boxPadding * 0.25; box.stopx = box.startx + box.width + 2 * boxPadding; box.stopy = box.starty + box.height + boxPadding * 0.75; box.stroke = 'rgb(0,0,0, 0.5)'; svgDraw.drawBox(diagram, box, conf); } if (hasBoxes) { bounds.bumpVerticalPos(conf.boxMargin); } // only draw popups for the top row of actors. const requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc); const { bounds: box } = bounds.getBounds(); if (box.startx === undefined) { box.startx = 0; } if (box.starty === undefined) { box.starty = 0; } if (box.stopx === undefined) { box.stopx = 0; } if (box.stopy === undefined) { box.stopy = 0; } // 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) ); 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 actors - The actors map * @param messages - A list of message objects to iterate * @param diagObj - The diagram object. * @returns The max message width of each actor. */ async function getMaxMessageWidthPerActor( actors: Map, messages: any[], diagObj: Diagram ): Promise> { const maxMessageWidthPerActor = {}; for (const msg of messages) { if (actors.get(msg.to) && actors.get(msg.from)) { const actor = actors.get(msg.to); // If this is the first actor, and the message is left of it, no need to calculate the margin if (msg.placement === diagObj.db.PLACEMENT.LEFTOF && !actor.prevActor) { continue; } // If this is the last actor, and the message is right of it, no need to calculate the margin if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF && !actor.nextActor) { continue; } const isNote = msg.placement !== undefined; const isMessage = !isNote; const textFont = isNote ? noteFont(conf) : messageFont(conf); const wrappedMessage = msg.wrap ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont) : msg.message; const messageDimensions = hasKatex(wrappedMessage) ? await calculateMathMLDimensions(msg.message, getConfig()) : 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] = common.getMax( maxMessageWidthPerActor[msg.to] || 0, messageWidth ); } else if (isMessage && msg.from === actor.prevActor) { maxMessageWidthPerActor[msg.from] = common.getMax( maxMessageWidthPerActor[msg.from] || 0, messageWidth ); } else if (isMessage && msg.from === msg.to) { maxMessageWidthPerActor[msg.from] = common.getMax( maxMessageWidthPerActor[msg.from] || 0, messageWidth / 2 ); maxMessageWidthPerActor[msg.to] = common.getMax( maxMessageWidthPerActor[msg.to] || 0, messageWidth / 2 ); } else if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) { maxMessageWidthPerActor[msg.from] = common.getMax( maxMessageWidthPerActor[msg.from] || 0, messageWidth ); } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) { maxMessageWidthPerActor[actor.prevActor] = common.getMax( maxMessageWidthPerActor[actor.prevActor] || 0, messageWidth ); } else if (msg.placement === diagObj.db.PLACEMENT.OVER) { if (actor.prevActor) { maxMessageWidthPerActor[actor.prevActor] = common.getMax( maxMessageWidthPerActor[actor.prevActor] || 0, messageWidth / 2 ); } if (actor.nextActor) { maxMessageWidthPerActor[msg.from] = common.getMax( maxMessageWidthPerActor[msg.from] || 0, messageWidth / 2 ); } } } } log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor); return maxMessageWidthPerActor; } const getRequiredPopupWidth = function (actor) { let requiredPopupWidth = 0; const textFont = actorFont(conf); for (const key in actor.links) { const labelDimensions = utils.calculateTextDimensions(key, textFont); const 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 actors - The actors map to calculate margins for * @param actorToMessageWidth - A map of actor key → max message width it holds * @param boxes - The boxes around the actors if any */ async function calculateActorMargins( actors: Map, actorToMessageWidth: Awaited>, boxes ) { let maxHeight = 0; for (const prop of actors.keys()) { const actor = actors.get(prop); if (actor.wrap) { actor.description = utils.wrapLabel( actor.description, conf.width - 2 * conf.wrapPadding, actorFont(conf) ); } const actDims = hasKatex(actor.description) ? await calculateMathMLDimensions(actor.description, getConfig()) : utils.calculateTextDimensions(actor.description, actorFont(conf)); actor.width = actor.wrap ? conf.width : common.getMax(conf.width, actDims.width + 2 * conf.wrapPadding); actor.height = actor.wrap ? common.getMax(actDims.height, conf.height) : conf.height; maxHeight = common.getMax(maxHeight, actor.height); } for (const actorKey in actorToMessageWidth) { const actor = actors.get(actorKey); if (!actor) { continue; } const nextActor = actors.get(actor.nextActor); // No need to space out an actor that doesn't have a next link if (!nextActor) { const messageWidth = actorToMessageWidth[actorKey]; const actorWidth = messageWidth + conf.actorMargin - actor.width / 2; actor.margin = common.getMax(actorWidth, conf.actorMargin); continue; } const messageWidth = actorToMessageWidth[actorKey]; const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2; actor.margin = common.getMax(actorWidth, conf.actorMargin); } let maxBoxHeight = 0; boxes.forEach((box) => { const textFont = messageFont(conf); let totalWidth = box.actorKeys.reduce((total, aKey) => { return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0)); }, 0); const standardBoxPadding = conf.boxMargin * 8; totalWidth += standardBoxPadding; totalWidth -= 2 * conf.boxTextMargin; if (box.wrap) { box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont); } const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont); maxBoxHeight = common.getMax(boxMsgDimensions.height, maxBoxHeight); const minWidth = common.getMax(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding); box.margin = conf.boxTextMargin; if (totalWidth < minWidth) { const missing = (minWidth - totalWidth) / 2; box.margin += missing; } }); boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight)); return common.getMax(maxHeight, conf.height); } const buildNoteModel = async function (msg, actors, diagObj) { const fromActor = actors.get(msg.from); const toActor = actors.get(msg.to); const startx = fromActor.x; const stopx = toActor.x; const shouldWrap = msg.wrap && msg.message; let textDimensions: { width: number; height: number; lineHeight?: number } = hasKatex(msg.message) ? await calculateMathMLDimensions(msg.message, getConfig()) : utils.calculateTextDimensions( shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message, noteFont(conf) ); const noteModel = { width: shouldWrap ? conf.width : common.getMax(conf.width, textDimensions.width + 2 * conf.noteMargin), height: 0, startx: fromActor.x, stopx: 0, starty: 0, stopy: 0, message: msg.message, }; if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) { noteModel.width = shouldWrap ? common.getMax(conf.width, textDimensions.width) : common.getMax( fromActor.width / 2 + toActor.width / 2, textDimensions.width + 2 * conf.noteMargin ); noteModel.startx = startx + (fromActor.width + conf.actorMargin) / 2; } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) { noteModel.width = shouldWrap ? common.getMax(conf.width, textDimensions.width + 2 * conf.noteMargin) : common.getMax( fromActor.width / 2 + toActor.width / 2, textDimensions.width + 2 * conf.noteMargin ); noteModel.startx = startx - noteModel.width + (fromActor.width - conf.actorMargin) / 2; } else if (msg.to === msg.from) { textDimensions = utils.calculateTextDimensions( shouldWrap ? utils.wrapLabel(msg.message, common.getMax(conf.width, fromActor.width), noteFont(conf)) : msg.message, noteFont(conf) ); noteModel.width = shouldWrap ? common.getMax(conf.width, fromActor.width) : common.getMax(fromActor.width, conf.width, textDimensions.width + 2 * conf.noteMargin); noteModel.startx = startx + (fromActor.width - noteModel.width) / 2; } else { noteModel.width = Math.abs(startx + fromActor.width / 2 - (stopx + toActor.width / 2)) + conf.actorMargin; noteModel.startx = startx < stopx ? startx + fromActor.width / 2 - conf.actorMargin / 2 : stopx + toActor.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, diagObj) { if ( ![ diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN, diagObj.db.LINETYPE.SOLID, diagObj.db.LINETYPE.DOTTED, diagObj.db.LINETYPE.SOLID_CROSS, diagObj.db.LINETYPE.DOTTED_CROSS, diagObj.db.LINETYPE.SOLID_POINT, diagObj.db.LINETYPE.DOTTED_POINT, diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED, ].includes(msg.type) ) { return {}; } const [fromLeft, fromRight] = activationBounds(msg.from, actors); const [toLeft, toRight] = activationBounds(msg.to, actors); const isArrowToRight = fromLeft <= toLeft; let startx = isArrowToRight ? fromRight : fromLeft; let stopx = isArrowToRight ? toLeft : toRight; // As the line width is considered, the left and right values will be off by 2. const isArrowToActivation = Math.abs(toLeft - toRight) > 2; /** * Adjust the value based on the arrow direction * @param value - The value to adjust * @returns The adjustment with correct sign to be added to the actual value. */ const adjustValue = (value: number) => { return isArrowToRight ? -value : value; }; if (msg.from === msg.to) { // This is a self reference, so we need to make sure the arrow is drawn correctly // There are many checks in the downstream rendering that checks for equality. // The lines on loops will be off by few pixels, but that's fine for now. stopx = startx; } else { /** * This is an edge case for the first activation. * Proper fix would require significant changes. * So, we set an activate flag in the message, and cross check that with isToActivation * In cases where the message is to an activation that was properly detected, we don't want to move the arrow head * The activation will not be detected on the first message, so we need to move the arrow head */ if (msg.activate && !isArrowToActivation) { stopx += adjustValue(conf.activationWidth / 2 - 1); } /** * Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead * This is not required for open arrows that don't have arrowheads */ if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) { stopx += adjustValue(3); } /** * Shorten start position of bidirectional arrow to accommodate for second arrowhead */ if ( [diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes( msg.type ) ) { startx -= adjustValue(3); } } const allBounds = [fromLeft, fromRight, toLeft, toRight]; const boundedWidth = Math.abs(startx - stopx); if (msg.wrap && msg.message) { msg.message = utils.wrapLabel( msg.message, common.getMax(boundedWidth + 2 * conf.wrapPadding, conf.width), messageFont(conf) ); } const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf)); return { width: common.getMax( msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding, boundedWidth + 2 * conf.wrapPadding, conf.width ), height: 0, startx, stopx, 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 = async function (messages, actors, _maxWidthPerActor, diagObj) { const loops = {}; const stack = []; let current, noteModel, msgModel; for (const msg of messages) { switch (msg.type) { case diagObj.db.LINETYPE.LOOP_START: case diagObj.db.LINETYPE.ALT_START: case diagObj.db.LINETYPE.OPT_START: case diagObj.db.LINETYPE.PAR_START: case diagObj.db.LINETYPE.PAR_OVER_START: case diagObj.db.LINETYPE.CRITICAL_START: case diagObj.db.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 diagObj.db.LINETYPE.ALT_ELSE: case diagObj.db.LINETYPE.PAR_AND: case diagObj.db.LINETYPE.CRITICAL_OPTION: if (msg.message) { current = stack.pop(); loops[current.id] = current; loops[msg.id] = current; stack.push(current); } break; case diagObj.db.LINETYPE.LOOP_END: case diagObj.db.LINETYPE.ALT_END: case diagObj.db.LINETYPE.OPT_END: case diagObj.db.LINETYPE.PAR_END: case diagObj.db.LINETYPE.CRITICAL_END: case diagObj.db.LINETYPE.BREAK_END: current = stack.pop(); loops[current.id] = current; break; case diagObj.db.LINETYPE.ACTIVE_START: { const actorRect = actors.get(msg.from ? msg.from : msg.to.actor); const stackedSize = actorActivations(msg.from ? msg.from : 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, enabled: true, }; bounds.activations.push(toAdd); } break; case diagObj.db.LINETYPE.ACTIVE_END: { const lastActorActivationIdx = bounds.activations .map((a) => a.actor) .lastIndexOf(msg.from); bounds.activations.splice(lastActorActivationIdx, 1).splice(0, 1); } break; } const isNote = msg.placement !== undefined; if (isNote) { noteModel = await buildNoteModel(msg, actors, diagObj); msg.noteModel = noteModel; stack.forEach((stk) => { current = stk; current.from = common.getMin(current.from, noteModel.startx); current.to = common.getMax(current.to, noteModel.startx + noteModel.width); current.width = common.getMax(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth; }); } else { msgModel = buildMessageModel(msg, actors, diagObj); msg.msgModel = msgModel; if (msgModel.startx && msgModel.stopx && stack.length > 0) { stack.forEach((stk) => { current = stk; if (msgModel.startx === msgModel.stopx) { const from = actors.get(msg.from); const to = actors.get(msg.to); current.from = common.getMin( from.x - msgModel.width / 2, from.x - from.width / 2, current.from ); current.to = common.getMax( to.x + msgModel.width / 2, to.x + from.width / 2, current.to ); current.width = common.getMax(current.width, Math.abs(current.to - current.from)) - conf.labelBoxWidth; } else { current.from = common.getMin(msgModel.startx, current.from); current.to = common.getMax(msgModel.stopx, current.to); current.width = common.getMax(current.width, msgModel.width) - conf.labelBoxWidth; } }); } } } bounds.activations = []; log.debug('Loop type widths:', loops); return loops; }; export default { bounds, drawActors, drawActorsPopup, setConf, draw, };