diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index a696be5de..6733c4ca4 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -13,4 +13,33 @@ describe('State diagram', () => { ); cy.get('svg'); }); + it('should render a simple state diagrams', () => { + imgSnapshotTest( + ` + stateDiagram + [*] --> State1 + State1 --> State2 + State1 --> State3 + State1 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); + it('should render a simple state diagrams with labels', () => { + imgSnapshotTest( + ` + stateDiagram + [*] --> State1 + State1 --> State2 : Transition 1 + State1 --> State3 : Transition 2 + State1 --> State4 : Transition 3 + State1 --> State5 : Transition 4 + State2 --> State3 : Transition 5 + State1 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); }); diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index d48c2ed66..0659258c6 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -106,7 +106,7 @@ line statement : idStatement DESCR | idStatement '-->' idStatement {yy.addRelation($1, $3);} - | idStatement '-->' idStatement DESCR + | idStatement '-->' idStatement DESCR {yy.addRelation($1, $3, $4.substr(1).trim());} | HIDE_EMPTY | scale WIDTH | COMPOSIT_STATE diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 08928bfbb..d3db25e20 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -41,7 +41,7 @@ export const getRelations = function() { return relations; }; -export const addRelation = function(_id1, _id2) { +export const addRelation = function(_id1, _id2, title) { let id1 = _id1; let id2 = _id2; let type1 = 'default'; @@ -56,10 +56,10 @@ export const addRelation = function(_id1, _id2) { id2 = 'end' + startCnt; type2 = 'end'; } - console.log(id1, id2); + console.log(id1, id2, title); addState(id1, type1); addState(id2, type2); - relations.push({ id1, id2 }); + relations.push({ id1, id2, title }); }; export const addMember = function(className, member) { diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 45555d4b4..d8918edd7 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -304,6 +304,24 @@ describe('state diagram, ', function() { note right of NotShooting : This is a note on a composite state `; + parser.parse(str); + }); + xit('should handle if statements', function() { + const str = `stateDiagram\n + [*] --> "Order Submitted" + if "Payment Accepted" then + -->[yes] "Pack products" + --> "Send parcel" + -right-> (*) + else + ->[no] "Send error message" + -->[Cancel Order] [*] + endif + } + + note right of NotShooting : This is a note on a composite state + `; + parser.parse(str); }); }); diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index e6c1d2426..be5dd1ea8 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -4,6 +4,7 @@ import graphlib from 'graphlibrary'; import { logger } from '../../logger'; import stateDb from './stateDb'; import { parser } from './parser/stateDiagram'; +import utils from '../../utils'; parser.yy = stateDb; @@ -136,7 +137,7 @@ const insertMarkers = function(elem) { .attr('markerHeight', 28) .attr('orient', 'auto') .append('path') - .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); + .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); }; const drawStart = function(elem, stateDef) { logger.info('Rendering class ' + stateDef); @@ -285,48 +286,67 @@ const drawEdge = function(elem, path, relation) { url = url.replace(/\)/g, '\\)'); } - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'Start' + ')' - ); + // svgPath.attr( + // 'marker-start', + // 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'Start' + ')' + // ); svgPath.attr( 'marker-end', 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' ); - let x, y; - const l = path.points.length; - if (l % 2 !== 0 && l > 1) { - const p1 = path.points[Math.floor(l / 2)]; - const p2 = path.points[Math.ceil(l / 2)]; - x = (p1.x + p2.x) / 2; - y = (p1.y + p2.y) / 2; - } else { - const p = path.points[Math.floor(l / 2)]; - x = p.x; - y = p.y; - } + // Figure ou where to put the label given the points + // let x, y; + // const l = path.points.length; + // if (l % 2 !== 0 && l > 1) { + // const p1 = path.points[Math.floor(l / 2)]; + // const p2 = path.points[Math.ceil(l / 2)]; + // x = (p1.x + p2.x) / 2; + // y = (p1.y + p2.y) / 2; + // } else { + // const p = path.points[Math.floor(l / 2)]; + // x = p.x; + // y = p.y; + // } + + // console.log('calcLabelPosition', utils); if (typeof relation.title !== 'undefined') { const g = elem.append('g').attr('class', 'classLabel'); const label = g .append('text') .attr('class', 'label') - .attr('x', x) - .attr('y', y) .attr('fill', 'red') .attr('text-anchor', 'middle') .text(relation.title); - window.label = label; - const bounds = label.node().getBBox(); + const { x, y } = utils.calcLabelPosition(path.points); + label.attr('x', x).attr('y', y); + const bounds = label.node().getBBox(); g.insert('rect', ':first-child') .attr('class', 'box') .attr('x', bounds.x - conf.padding / 2) .attr('y', bounds.y - conf.padding / 2) .attr('width', bounds.width + conf.padding) .attr('height', bounds.height + conf.padding); + + // Debug points + // path.points.forEach(point => { + // g.append('circle') + // .style('stroke', 'red') + // .style('fill', 'red') + // .attr('r', 1) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); + + // g.append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 1) + // .attr('cx', x) + // .attr('cy', y); } edgeCount++; @@ -338,7 +358,7 @@ const drawEdge = function(elem, path, relation) { * @param {*} stateDef */ const drawState = function(elem, stateDef) { - logger.info('Rendering class ' + stateDef); + // logger.info('Rendering class ' + stateDef); const addTspan = function(textEl, txt, isFirst) { const tSpan = textEl @@ -416,14 +436,11 @@ export const draw = function(text, id) { // metadata about the node. In this case we're going to add labels to each of // our nodes. graph.setNode(node.id, node); - logger.info('Org height: ' + node.height); + // logger.info('Org height: ' + node.height); } const relations = stateDb.getRelations(); relations.forEach(function(relation) { - logger.info( - 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) - ); graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { relation: relation }); diff --git a/src/utils.js b/src/utils.js index 080c54c96..2d11e7ef5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -73,8 +73,56 @@ export const interpolateToCurve = (interpolate, defaultCurve) => { return d3[curveName] || defaultCurve; }; +const distance = (p1, p2) => + p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0; + +const traverseEdge = points => { + let prevPoint; + let totalDistance = 0; + + points.forEach(point => { + totalDistance += distance(point, prevPoint); + prevPoint = point; + }); + + // Traverse half of total distance along points + const distanceToLabel = totalDistance / 2; + + let remainingDistance = distanceToLabel; + let center; + prevPoint = undefined; + points.forEach(point => { + if (prevPoint && !center) { + const vectorDistance = distance(point, prevPoint); + if (vectorDistance < remainingDistance) { + remainingDistance -= vectorDistance; + } else { + // The point is remainingDistance from prevPoint in the vector between prevPoint and point + // Calculate the coordinates + const distanceRatio = remainingDistance / vectorDistance; + if (distanceRatio <= 0) center = prevPoint; + if (distanceRatio >= 1) center = { x: point.x, y: point.y }; + if (distanceRatio > 0 && distanceRatio < 1) { + center = { + x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, + y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y + }; + } + } + } + prevPoint = point; + }); + return center; +}; + +const calcLabelPosition = points => { + const p = traverseEdge(points); + return p; +}; + export default { detectType, isSubstringInArray, - interpolateToCurve + interpolateToCurve, + calcLabelPosition };