From cdc2a1f3d1199c5fb053490a8757d710f89cc2ea Mon Sep 17 00:00:00 2001 From: Junjue Wang Date: Mon, 14 Jan 2019 18:33:38 -0500 Subject: [PATCH] add in multiple transition support fix #5 --- .../package-lock.json | 8 +- .../statemachine-editor-react/package.json | 1 + .../statemachine-editor-react/src/diagram.js | 131 +++++++++++++++++- .../src/elementModal.js | 3 + .../statemachine-editor-react/src/infoBox.js | 14 +- .../statemachine-editor-react/src/utils.js | 44 +++--- 6 files changed, 167 insertions(+), 34 deletions(-) diff --git a/gabrieltool/statemachine-editor-react/package-lock.json b/gabrieltool/statemachine-editor-react/package-lock.json index 54b7ac2..1791c66 100644 --- a/gabrieltool/statemachine-editor-react/package-lock.json +++ b/gabrieltool/statemachine-editor-react/package-lock.json @@ -5812,13 +5812,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5835,8 +5833,7 @@ }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", @@ -5965,7 +5962,6 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/gabrieltool/statemachine-editor-react/package.json b/gabrieltool/statemachine-editor-react/package.json index c03f924..2125609 100644 --- a/gabrieltool/statemachine-editor-react/package.json +++ b/gabrieltool/statemachine-editor-react/package.json @@ -10,6 +10,7 @@ "google-protobuf": "^3.6.1", "jointjs": "^2.2.1", "jquery": "^3.1.1", + "lodash": "^4.17.11", "match-sorter": "^2.3.0", "react": "^16.7.0", "react-bootstrap": "^1.0.0-beta.3", diff --git a/gabrieltool/statemachine-editor-react/src/diagram.js b/gabrieltool/statemachine-editor-react/src/diagram.js index 2e97e17..912b420 100644 --- a/gabrieltool/statemachine-editor-react/src/diagram.js +++ b/gabrieltool/statemachine-editor-react/src/diagram.js @@ -1,6 +1,8 @@ import React, { Component } from "react"; import $ from "jquery"; import joint from "jointjs"; +import _ from 'lodash'; + // define custom state machine JointJS elements joint.shapes.basic.Circle.define("fsa.State", { @@ -27,6 +29,126 @@ joint.shapes.standard.Link.define("fsa.CustomArrow", { smooth: true }); +function adjustVertices(graph, cell) { + // if `cell` is a view, find its model + cell = cell.model || cell; + + if (cell instanceof joint.dia.Element) { + // `cell` is an element + + _.chain(graph.getConnectedLinks(cell)) + .groupBy(function(link) { + // the key of the group is the model id of the link's source or target + // cell id is omitted + return _.omit([link.source().id, link.target().id], cell.id)[0]; + }) + .each(function(group, key) { + // if the member of the group has both source and target model + // then adjust vertices + if (key !== "undefined") adjustVertices(graph, _.first(group)); + }) + .value(); + + return; + } + + // `cell` is a link + // get its source and target model IDs + var sourceId = cell.get("source").id || cell.previous("source").id; + var targetId = cell.get("target").id || cell.previous("target").id; + + // if one of the ends is not a model + // (if the link is pinned to paper at a point) + // the link is interpreted as having no siblings + if (!sourceId || !targetId) { + // no vertices needed + cell.unset("vertices"); + return; + } + + // identify link siblings + var siblings = graph.getLinks().filter(function(sibling) { + var siblingSourceId = sibling.source().id; + var siblingTargetId = sibling.target().id; + + // if source and target are the same + // or if source and target are reversed + return ( + (siblingSourceId === sourceId && siblingTargetId === targetId) || + (siblingSourceId === targetId && siblingTargetId === sourceId) + ); + }); + + var numSiblings = siblings.length; + switch (numSiblings) { + case 0: { + // the link has no siblings + break; + } + default: { + if (numSiblings === 1) { + // there is only one link + // no vertices needed + cell.unset("vertices"); + } + + // there are multiple siblings + // we need to create vertices + + // find the middle point of the link + var sourceCenter = graph + .getCell(sourceId) + .getBBox() + .center(); + var targetCenter = graph + .getCell(targetId) + .getBBox() + .center(); + var midPoint = joint.g.Line(sourceCenter, targetCenter).midpoint(); + + // find the angle of the link + var theta = sourceCenter.theta(targetCenter); + + // constant + // the maximum distance between two sibling links + var GAP = 20; + + _.each(siblings, function(sibling, index) { + // we want offset values to be calculated as 0, 20, 20, 40, 40, 60, 60 ... + var offset = GAP * Math.ceil(index / 2); + + // place the vertices at points which are `offset` pixels perpendicularly away + // from the first link + // + // as index goes up, alternate left and right + // + // ^ odd indices + // | + // |----> index 0 sibling - centerline (between source and target centers) + // | + // v even indices + var sign = index % 2 ? 1 : -1; + + // to assure symmetry, if there is an even number of siblings + // shift all vertices leftward perpendicularly away from the centerline + if (numSiblings % 2 === 0) { + offset -= (GAP / 2) * sign; + } + + // make reverse links count the same as non-reverse + var reverse = theta < 180 ? 1 : -1; + + // we found the vertex + var angle = joint.g.toRad(theta + sign * reverse * 90); + var vertex = joint.g.Point.fromPolar(offset, angle, midPoint); + + // replace vertices array with `vertex` + sibling.vertices([vertex]); + }); + } + } +} + const create_transition_cell = (source, target, label) => { var cell = new joint.shapes.fsa.CustomArrow({ source: { @@ -72,6 +194,11 @@ export class Diagram extends Component { constructor(props) { super(props); this.graph = new joint.dia.Graph(); + // bind `graph` to the `adjustVertices` function + var adjustGraphVertices = _.partial(adjustVertices, this.graph); + // adjust vertices when a cell is removed or its source/target was changed + this.graph.on('add remove change:source change:target', adjustGraphVertices); + this.state_shape_width = 50; this.state_shape_height = 50; this.state_spacing_x = 250; @@ -124,8 +251,8 @@ export class Diagram extends Component { state.getName() ); // mark start state - if (fsm.getStartState() === state.getName()){ - cell.attr('circle/stroke-width', '5'); + if (fsm.getStartState() === state.getName()) { + cell.attr("circle/stroke-width", "5"); } this.addGraphCellWithRef(cell, state); return null; diff --git a/gabrieltool/statemachine-editor-react/src/elementModal.js b/gabrieltool/statemachine-editor-react/src/elementModal.js index addf19a..e223d38 100644 --- a/gabrieltool/statemachine-editor-react/src/elementModal.js +++ b/gabrieltool/statemachine-editor-react/src/elementModal.js @@ -292,6 +292,7 @@ const createCallableArgMultiFields = (args, index, errors) => { component={CallableArgField} label={key} placeholder={args[key]} + defaultValue="" validate={isEmpty} /> {addFieldError(errors, `callable.${index}.args.${key}`)} @@ -333,6 +334,7 @@ const createTransitionBasicFields = (fsm, form, errors) => { component={BSFormikField} type="text" label="Audio Instruction" + defaultValue="" /> { component={BSFormikField} type="text" label="Video Instruction" + defaultValue="" /> ); diff --git a/gabrieltool/statemachine-editor-react/src/infoBox.js b/gabrieltool/statemachine-editor-react/src/infoBox.js index bac873f..8dd3b22 100644 --- a/gabrieltool/statemachine-editor-react/src/infoBox.js +++ b/gabrieltool/statemachine-editor-react/src/infoBox.js @@ -30,11 +30,13 @@ class InfoBox extends Component { URL.revokeObjectURL(this.imageInstUrl); } if (element.getInstruction()) { - let blob = new Blob([element.getInstruction().getImage()], { - type: "image" - }); - this.imageInstUrl = URL.createObjectURL(blob); - res.imageInstUrl = this.imageInstUrl; + if (element.getInstruction().getImage()) { + let blob = new Blob([element.getInstruction().getImage()], { + type: "image" + }); + this.imageInstUrl = URL.createObjectURL(blob); + res.imageInstUrl = this.imageInstUrl; + } } return res; } @@ -114,7 +116,7 @@ class InfoBox extends Component { Image: instruction ) : ( - Image: undefined + Image: )} Video: {element.getInstruction().getVideo()} diff --git a/gabrieltool/statemachine-editor-react/src/utils.js b/gabrieltool/statemachine-editor-react/src/utils.js index 39f68e7..3b26973 100644 --- a/gabrieltool/statemachine-editor-react/src/utils.js +++ b/gabrieltool/statemachine-editor-react/src/utils.js @@ -25,7 +25,7 @@ function isObject(o) { // 'a': ['property': 'test'] // } // e.g. obj['a.0.property'] -export const getPropertyByString = function (o, s) { +export const getPropertyByString = function(o, s) { if (s) { s = s.replace(/^\./, ""); // strip a leading dot var a = s.split("."); @@ -43,7 +43,7 @@ export const getPropertyByString = function (o, s) { } }; -export const findStatePbByName = function (stateName, fsm) { +export const findStatePbByName = function(stateName, fsm) { let result = null; fsm.getStatesList().map(state => { if (state.getName() === stateName) { @@ -54,7 +54,7 @@ export const findStatePbByName = function (stateName, fsm) { return result; }; -export const findTransitionOriginateState = function (transition, fsm) { +export const findTransitionOriginateState = function(transition, fsm) { let result = null; fsm.getStatesList().map(state => { state.getTransitionsList().map(curTransition => { @@ -68,7 +68,7 @@ export const findTransitionOriginateState = function (transition, fsm) { return result; }; -const callableToFormValues = function (elementCallables) { +const callableToFormValues = function(elementCallables) { let result = []; elementCallables.map(elementCallableItem => { let item = {}; @@ -86,7 +86,7 @@ const callableToFormValues = function (elementCallables) { return result; }; -const getElementCallables = function (element) { +const getElementCallables = function(element) { const elementType = getFSMElementType(element); let elementCallables = null; switch (elementType) { @@ -99,14 +99,14 @@ const getElementCallables = function (element) { default: throw new Error( "Unsupported Element Type: " + - elementType + - ". Failed to add a new element" + elementType + + ". Failed to add a new element" ); } return elementCallables; }; -export const elementToFormValues = function (element, fsm) { +export const elementToFormValues = function(element, fsm) { const values = {}; values.callable = []; const elementType = getFSMElementType(element); @@ -116,7 +116,7 @@ export const elementToFormValues = function (element, fsm) { // type specific attrs switch (elementType) { case FSMElementType.STATE: - values.isStartState = (element.getName() === fsm.getStartState()); + values.isStartState = element.getName() === fsm.getStartState(); break; case FSMElementType.TRANSITION: values.to = element.getNextState(); @@ -128,7 +128,9 @@ export const elementToFormValues = function (element, fsm) { break; default: throw new Error( - "Unsupported Element Type: " + elementType + ". Failed to add a new element" + "Unsupported Element Type: " + + elementType + + ". Failed to add a new element" ); break; } @@ -138,7 +140,7 @@ export const elementToFormValues = function (element, fsm) { return values; }; -const formCallableToElementCallable = function ( +const formCallableToElementCallable = function( callbleFormValue, setFunc, callablePbType, @@ -171,7 +173,7 @@ const formCallableToElementCallable = function ( * @param {*} newName * @param {*} fsm */ -const setStateName = function (element, newName, aux) { +const setStateName = function(element, newName, aux) { const { fsm } = aux; let oldName = element.getName(); if (oldName) { @@ -190,7 +192,7 @@ const setStateName = function (element, newName, aux) { element.setName(newName); }; -const setTransitionFromState = function (element, newFromStateName, aux) { +const setTransitionFromState = function(element, newFromStateName, aux) { const { fsm } = aux; let oldFromState = findTransitionOriginateState(element, fsm); if (newFromStateName !== oldFromState.getName()) { @@ -210,7 +212,7 @@ const setTransitionFromState = function (element, newFromStateName, aux) { * @param {} formValue * @param {*} element: the FSM element to be set. */ -export const formValuesToElement = function (formValue, fsm, type, initElement) { +export const formValuesToElement = function(formValue, fsm, type, initElement) { // create or use appropriate element based on type let element = null; if (initElement === null || initElement === undefined) { @@ -237,10 +239,10 @@ export const formValuesToElement = function (formValue, fsm, type, initElement) // deal with type specific fields switch (type) { case FSMElementType.STATE: - setStateName(element, formValue["name"], { fsm: fsm }); + setStateName(element, formValue.name, { fsm: fsm }); // set start state - if (formValue["isStartState"]) { - fsm.setStartState(formValue["name"]); + if (formValue.isStartState) { + fsm.setStartState(formValue.name); } // add processors formCallableToElementCallable( @@ -261,9 +263,11 @@ export const formValuesToElement = function (formValue, fsm, type, initElement) element.setNextState(formValue.to); // instruction let instPb = new fsmPb.Instruction(); - instPb.setAudio(formValue.instruction.audio); - instPb.setImage(formValue.instruction.image); - instPb.setVideo(formValue.instruction.video); + if (formValue.instruction) { + instPb.setAudio(formValue.instruction.audio); + instPb.setImage(formValue.instruction.image); + instPb.setVideo(formValue.instruction.video); + } element.setInstruction(instPb); // add predicates formCallableToElementCallable(