diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index c386a8bbd..45cbfa047 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -508,7 +508,7 @@ context('Sequence diagram', () => { it('should overide config with directive settings', () => { imgSnapshotTest( ` - %%{init: { "sequence": { "mirrorActors": true }}}%% + %%{init: { "config": { "mirrorActors": true }}}%% sequenceDiagram Alice->>Bob: I'm short note left of Alice: config set to mirrorActors: false
directive set to mirrorActors: true @@ -520,7 +520,7 @@ context('Sequence diagram', () => { it('should overide config with directive settings', () => { imgSnapshotTest( ` - %%{init: { "sequence": { "mirrorActors": false }}}%% + %%{init: { "config": { "mirrorActors": false }}}%% sequenceDiagram %%{config: { "mirrorActors": false} }%% Alice->>Bob: I'm short diff --git a/src/diagrams/sequence/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js index 3d38f9860..8869eb50c 100644 --- a/src/diagrams/sequence/sequenceDb.js +++ b/src/diagrams/sequence/sequenceDb.js @@ -202,7 +202,7 @@ export const clear = function() { export const parseMessage = function(str) { const _str = str.trim(); - const retVal = { + return { text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(), wrap: _str.match(/^[:]?(?:no)?wrap:/) === null @@ -213,8 +213,6 @@ export const parseMessage = function(str) { ? false : autoWrap() }; - logger.debug(`ParseMessage[${str}] [${JSON.stringify(retVal, null, 2)}`); - return retVal; }; export const LINETYPE = { diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index f348cc029..47f9aef11 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -46,6 +46,8 @@ const conf = { // width of activation box activationWidth: 10, + labelBoxWidth: 50, + labelBoxHeight: 20, // text placement as: tspan | fo | old only text as before textPlacement: 'tspan', @@ -91,9 +93,19 @@ export const bounds = { sequenceItems: [], activations: [], + models: { + loops: [], + messages: [], + notes: [] + }, init: function() { this.sequenceItems = []; this.activations = []; + this.models = { + loops: [], + messages: [], + notes: [] + }; this.data = { startx: undefined, stopx: undefined, @@ -181,6 +193,7 @@ export const bounds = { title: title.message, wrap: title.wrap, width: title.width, + height: 0, fill: fill }; }, @@ -194,7 +207,7 @@ export const bounds = { const loop = this.sequenceItems.pop(); loop.sections = loop.sections || []; loop.sectionTitles = loop.sectionTitles || []; - loop.sections.push(bounds.getVerticalPos()); + loop.sections.push({ y: bounds.getVerticalPos(), height: 0 }); loop.sectionTitles.push(message); 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 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'); 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; @@ -287,52 +268,41 @@ const drawNote = function(elem, noteModel) { /** * Draws a message - * @param elem - * @param startx - * @param stopx - * @param verticalPos - * @param msg - * @param sequenceIndex + * @param g - the parent of the message element + * @param msgModel - the model containing fields describing a message */ -const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) { - const g = elem.append('g'); - const txtCenter = startx + (stopx - startx) / 2; +const drawMessage = function(g, msgModel) { + const { startx, stopx, starty: verticalPos, message, type, sequenceIndex, wrap } = msgModel; + 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; - 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'); - } - } + const lines = message.split(common.lineBreakRegex).length - 1 > 0 ? 1 : 0; - 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; 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); bounds.insert( startx - dx, @@ -395,9 +365,9 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde // Make an SVG Container // Draw the line if ( - msg.type === parser.yy.LINETYPE.DOTTED || - msg.type === parser.yy.LINETYPE.DOTTED_CROSS || - msg.type === parser.yy.LINETYPE.DOTTED_OPEN + type === parser.yy.LINETYPE.DOTTED || + type === parser.yy.LINETYPE.DOTTED_CROSS || + type === parser.yy.LINETYPE.DOTTED_OPEN ) { line.style('stroke-dasharray', '3, 3'); 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', 'black'); 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)'); } - 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)'); } @@ -452,8 +422,8 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) { const actor = actors[actorKeys[i]]; // Add some rendering data to the object - actor.width = actor.width ? actor.width : conf.width; - actor.height = conf.height; + 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; @@ -491,9 +461,9 @@ const actorActivations = function(actor) { }); }; -const actorFlowVerticaBounds = function(actor) { +const actorFlowVerticaBounds = function(actor, actors) { // handle multiple stacked activations for same actor - const actorObj = parser.yy.getActors()[actor]; + const actorObj = actors[actor]; const activations = actorActivations(actor); const left = activations.reduce(function(acc, activation) { @@ -506,28 +476,23 @@ const actorFlowVerticaBounds = function(actor) { }; function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) { - let heightAdjust = 0; bounds.bumpVerticalPos(preMargin); - if (msg.message && loopWidths[msg.message]) { - let loopWidth = loopWidths[msg.message].width; + let heightAdjust = postMargin; + if (msg.id && msg.message && loopWidths[msg.id]) { + let loopWidth = loopWidths[msg.id].width; let textConf = conf.messageFont(); - msg.message = utils.wrapLabel( - `[${msg.message}]`, - loopWidth - 20 - 2 * conf.wrapPadding, - textConf - ); + msg.message = utils.wrapLabel(`[${msg.message}]`, loopWidth - 2 * conf.wrapPadding, textConf); + msg.width = loopWidth; - heightAdjust = Math.max( - 0, - utils.calculateTextHeight(msg.message, textConf) - (preMargin + postMargin) - ); + const textHeight = utils.calculateTextHeight(msg.message, textConf); + heightAdjust += textHeight; } 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 id */ @@ -576,15 +541,15 @@ export const draw = function(text, id) { // Draw the messages/signals let sequenceIndex = 1; messages.forEach(function(msg) { - let loopData, noteModel, msgModel; + let loopModel, noteModel, msgModel; switch (msg.type) { case parser.yy.LINETYPE.NOTE: bounds.bumpVerticalPos(conf.boxMargin); noteModel = msg.noteModel; noteModel.y = bounds.getVerticalPos(); - logger.debug('noteModel', noteModel); drawNote(diagram, noteModel); + bounds.models.notes.push(noteModel); break; case parser.yy.LINETYPE.ACTIVE_START: bounds.newActivation(msg, diagram, actors); @@ -602,18 +567,21 @@ export const draw = function(text, id) { ); break; case parser.yy.LINETYPE.LOOP_END: - loopData = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopData, 'loop', conf); - bounds.bumpVerticalPos(conf.boxMargin); + loopModel = bounds.endLoop(); + svgDraw.drawLoop(diagram, loopModel, 'loop', conf); + bounds.models.loops.push(loopModel); + bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; case parser.yy.LINETYPE.RECT_START: - bounds.bumpVerticalPos(conf.boxMargin); - bounds.newLoop(undefined, msg.message); - bounds.bumpVerticalPos(conf.boxMargin); + adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message => + bounds.newLoop(undefined, message.message) + ); break; case parser.yy.LINETYPE.RECT_END: - svgDraw.drawBackgroundRect(diagram, bounds.endLoop()); - bounds.bumpVerticalPos(conf.boxMargin); + loopModel = bounds.endLoop(); + svgDraw.drawBackgroundRect(diagram, loopModel); + bounds.models.loops.push(loopModel); + bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; case parser.yy.LINETYPE.OPT_START: adjustLoopHeightForWrap( @@ -625,9 +593,10 @@ export const draw = function(text, id) { ); break; case parser.yy.LINETYPE.OPT_END: - loopData = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopData, 'opt', conf); - bounds.bumpVerticalPos(conf.boxMargin); + loopModel = bounds.endLoop(); + svgDraw.drawLoop(diagram, loopModel, 'opt', conf); + bounds.models.loops.push(loopModel); + bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; case parser.yy.LINETYPE.ALT_START: adjustLoopHeightForWrap( @@ -639,14 +608,19 @@ export const draw = function(text, id) { ); break; case parser.yy.LINETYPE.ALT_ELSE: - adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message => - bounds.addSectionToLoop(message) + adjustLoopHeightForWrap( + loopWidths, + msg, + conf.boxMargin + conf.boxTextMargin, + conf.boxMargin, + message => bounds.addSectionToLoop(message) ); break; case parser.yy.LINETYPE.ALT_END: - loopData = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopData, 'alt', conf); - bounds.bumpVerticalPos(conf.boxMargin); + loopModel = bounds.endLoop(); + svgDraw.drawLoop(diagram, loopModel, 'alt', conf); + bounds.models.loops.push(loopModel); + bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; case parser.yy.LINETYPE.PAR_START: adjustLoopHeightForWrap( @@ -658,14 +632,19 @@ export const draw = function(text, id) { ); break; case parser.yy.LINETYPE.PAR_AND: - adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message => - bounds.addSectionToLoop(message) + adjustLoopHeightForWrap( + loopWidths, + msg, + conf.boxMargin + conf.boxTextMargin, + conf.boxMargin, + message => bounds.addSectionToLoop(message) ); break; case parser.yy.LINETYPE.PAR_END: - loopData = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopData, 'par', conf); - bounds.bumpVerticalPos(conf.boxMargin); + loopModel = bounds.endLoop(); + svgDraw.drawLoop(diagram, loopModel, 'par', conf); + bounds.models.loops.push(loopModel); + bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; default: try { @@ -673,14 +652,9 @@ export const draw = function(text, id) { bounds.bumpVerticalPos(conf.messageMargin); msgModel = msg.msgModel; msgModel.starty = bounds.getVerticalPos(); - drawMessage( - diagram, - msgModel.startx, - msgModel.stopx, - msgModel.starty, - msgModel, - sequenceIndex - ); + msgModel.sequenceIndex = sequenceIndex; + drawMessage(diagram, msgModel); + bounds.models.messages.push(msgModel); bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.starty); } catch (e) { logger.error('error while drawing message', e); @@ -750,6 +724,7 @@ export const draw = function(text, id) { ' ' + (height + extraVertForTitle) ); + logger.debug('bounds', bounds); }; /** @@ -784,10 +759,10 @@ const getMaxMessageWidthPerActor = function(actors, messages) { const textFont = isNote ? conf.noteFont() : conf.messageFont(); 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; const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont); - const messageWidth = messageDimensions.width; + const messageWidth = messageDimensions.width + 2 * conf.wrapPadding; /* * 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; }; @@ -915,7 +890,7 @@ const buildNoteModel = function(msg, actors) { }; if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) { noteModel.width = shouldWrap - ? conf.width + ? Math.max(conf.width, textDimensions.width) : Math.max( actors[msg.from].width / 2 + actors[msg.to].width / 2, textDimensions.width + 2 * conf.noteMargin @@ -923,7 +898,7 @@ const buildNoteModel = function(msg, actors) { noteModel.x = startx + (actors[msg.from].width + conf.actorMargin) / 2; } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) { noteModel.width = shouldWrap - ? conf.width + ? 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 @@ -959,7 +934,7 @@ const buildNoteModel = function(msg, actors) { return noteModel; }; -const buildMessageModel = function(msg) { +const buildMessageModel = function(msg, actors) { let process = false; if ( [ @@ -976,13 +951,13 @@ const buildMessageModel = function(msg) { if (!process) { return {}; } - const fromBounds = actorFlowVerticaBounds(msg.from); - const toBounds = actorFlowVerticaBounds(msg.to); + const fromBounds = actorFlowVerticaBounds(msg.from, actors); + const toBounds = actorFlowVerticaBounds(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 msgModel = { - width: Math.abs(toBounds[toIdx] - fromBounds[fromIdx]) + conf.messageMargin * 2, + width: Math.abs(toBounds[toIdx] - fromBounds[fromIdx]), height: 0, startx: fromBounds[fromIdx], stopx: toBounds[toIdx], @@ -990,13 +965,14 @@ const buildMessageModel = function(msg) { stopy: 0, message: msg.message, type: msg.type, + wrap: msg.wrap, fromBounds: Math.min.apply(null, allBounds), toBounds: Math.max.apply(null, allBounds) }; if (msg.wrap && msg.message && !common.lineBreakRegex.test(msg.message)) { msgModel.message = utils.wrapLabel( msg.message, - Math.max(msgModel.width, conf.width + conf.messageMargin * 2), + Math.max(msgModel.width, conf.width), conf.messageFont() ); } @@ -1009,12 +985,14 @@ const calculateLoopBounds = function(messages, actors) { let current, noteModel, msgModel; messages.forEach(function(msg) { + msg.id = utils.generateId(); 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: stack.push({ + id: msg.id, msg: msg.message, from: Number.MAX_SAFE_INTEGER, to: Number.MIN_SAFE_INTEGER, @@ -1025,7 +1003,8 @@ const calculateLoopBounds = function(messages, actors) { case parser.yy.LINETYPE.PAR_AND: if (msg.message) { current = stack.pop(); - loops[msg.message] = current; + loops[current.id] = current; + loops[msg.id] = current; stack.push(current); } break; @@ -1034,23 +1013,52 @@ const calculateLoopBounds = function(messages, actors) { case parser.yy.LINETYPE.OPT_END: case parser.yy.LINETYPE.PAR_END: 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; } const isNote = msg.placement !== undefined; if (isNote) { noteModel = buildNoteModel(msg, actors); msg.noteModel = noteModel; + let depth = 0; stack.forEach(stk => { current = stk; current.from = Math.min(current.from, noteModel.x); 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 { - msgModel = buildMessageModel(msg); + msgModel = buildMessageModel(msg, actors); msg.msgModel = msgModel; if (msg.from && msg.to && stack.length > 0) { + let depth = 0; stack.forEach(stk => { current = stk; let from = actors[msg.from]; @@ -1058,7 +1066,7 @@ const calculateLoopBounds = function(messages, actors) { if (from.x === to.x) { current.from = Math.min(current.from, from.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 { if (from.x < to.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.to = Math.max(current.to, from.x); } - current.width = Math.max( - current.width, - Math.abs(current.from - current.to) - 20 + 2 * conf.wrapPadding - ); + current.width = + Math.max(current.width, Math.abs(current.from - current.to)) - + 50 - + conf.boxMargin * depth; } + depth++; }); } } }); + bounds.activations = []; logger.debug('Loop type widths:', loops); return loops; }; diff --git a/src/diagrams/sequence/svgDraw.js b/src/diagrams/sequence/svgDraw.js index 7c310dd7d..72175e6df 100644 --- a/src/diagrams/sequence/svgDraw.js +++ b/src/diagrams/sequence/svgDraw.js @@ -51,6 +51,35 @@ export const drawText = function(elem, textData) { 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++) { let line = lines[i]; if ( @@ -65,7 +94,10 @@ export const drawText = function(elem, textData) { textElem.attr('x', textData.x); textElem.attr('y', yfunc()); 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') { textElem.style('font-family', textData.fontFamily); @@ -88,13 +120,16 @@ export const drawText = function(elem, textData) { textElem.attr('dy', dy); } - const span = textElem.append('tspan'); - span.attr('x', textData.x); - if (typeof textData.fill !== 'undefined') { - span.attr('fill', textData.fill); + if (textData.tspan) { + const span = textElem.append('tspan'); + span.attr('x', textData.x); + if (typeof textData.fill !== 'undefined') { + span.attr('fill', textData.fill); + } + span.text(line); + } else { + textElem.text(line); } - span.text(line); - if ( typeof textData.valign !== 'undefined' && typeof textData.textMargin !== 'undefined' && @@ -107,7 +142,7 @@ export const drawText = function(elem, textData) { textElems.push(textElem); } - return textElems.length === 1 ? textElems[0] : textElems; + return textElems; }; export const drawLabel = function(elem, txtObject) { @@ -135,17 +170,18 @@ export const drawLabel = function(elem, txtObject) { ); } 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'); - txtObject.y = txtObject.y + txtObject.labelMargin; - txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin; - return drawText(elem, txtObject); + txtObject.y = txtObject.y + txtObject.height / 2; + + drawText(elem, txtObject); + return polygon; }; 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 actor - The actor to draw. * @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); if (typeof bounds.sections !== 'undefined') { 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(); txt.text = labelText; txt.x = bounds.startx; txt.y = bounds.starty; - txt.labelMargin = 1.5 * 10; // This is the small box that says "loop" - txt.fontFamily = conf.messageFontFamily; - txt.fontSize = minSize; - txt.fontWeight = conf.messageFontWeight; - txt.class = 'labelText'; // Its size & position are fixed. + const msgFont = conf.messageFont(); + txt.fontFamily = msgFont.fontFamily; + txt.fontSize = msgFont.fontSize; + txt.fontWeight = msgFont.fontWeight; + 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); - let labelBoxWidth = (labelElem._groups || labelElem)[0][0].getBBox().width; + drawLabel(g, txt); txt = getTextObj(); 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.anchor = 'middle'; txt.class = 'loopText'; - txt.fontFamily = conf.messageFontFamily; - txt.fontSize = minSize; - txt.fontWeight = conf.messageFontWeight; + txt.fontFamily = msgFont.fontFamily; + txt.fontSize = msgFont.fontSize; + txt.fontWeight = msgFont.fontWeight; txt.wrap = true; - drawText(g, txt); + let textElem = drawText(g, txt); if (typeof bounds.sectionTitles !== 'undefined') { bounds.sectionTitles.forEach(function(item, idx) { if (item.message) { txt.text = item.message; 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.anchor = 'middle'; - txt.fontFamily = conf.messageFontFamily; - txt.fontSize = minSize; - txt.fontWeight = conf.messageFontWeight; + txt.valign = 'middle'; + txt.tspan = false; + txt.fontFamily = msgFont.fontFamily; + txt.fontSize = msgFont.fontSize; + txt.fontWeight = msgFont.fontWeight; 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; }; @@ -380,13 +426,14 @@ export const getTextObj = function() { x: 0, y: 0, fill: undefined, - anchor: 'start', + anchor: undefined, style: '#666', - width: 100, - height: 100, + width: undefined, + height: undefined, textMargin: 0, rx: 0, ry: 0, + tspan: true, valign: undefined }; }; diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 9e63251dd..4132ead7b 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -335,7 +335,17 @@ const config = { * This sets the auto-wrap padding for the diagram (sides only) * **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 }, /**