Skip to content

Commit

Permalink
add in multiple transition support fix #5
Browse files Browse the repository at this point in the history
  • Loading branch information
junjuew committed Jan 14, 2019
1 parent d123ea6 commit cdc2a1f
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 34 deletions.
8 changes: 2 additions & 6 deletions gabrieltool/statemachine-editor-react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gabrieltool/statemachine-editor-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
131 changes: 129 additions & 2 deletions gabrieltool/statemachine-editor-react/src/diagram.js
Original file line number Diff line number Diff line change
@@ -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", {
Expand All @@ -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: {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions gabrieltool/statemachine-editor-react/src/elementModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)}
Expand Down Expand Up @@ -333,6 +334,7 @@ const createTransitionBasicFields = (fsm, form, errors) => {
component={BSFormikField}
type="text"
label="Audio Instruction"
defaultValue=""
/>
<Field
name="instruction.image"
Expand All @@ -344,6 +346,7 @@ const createTransitionBasicFields = (fsm, form, errors) => {
component={BSFormikField}
type="text"
label="Video Instruction"
defaultValue=""
/>
</>
);
Expand Down
14 changes: 8 additions & 6 deletions gabrieltool/statemachine-editor-react/src/infoBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -114,7 +116,7 @@ class InfoBox extends Component {
Image: <img src={res.imageInstUrl} alt="instruction" />
</ListGroupItem>
) : (
<ListGroupItem>Image: undefined</ListGroupItem>
<ListGroupItem>Image: </ListGroupItem>
)}
<ListGroupItem>
Video: {element.getInstruction().getVideo()}
Expand Down
44 changes: 24 additions & 20 deletions gabrieltool/statemachine-editor-react/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(".");
Expand All @@ -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) {
Expand All @@ -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 => {
Expand All @@ -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 = {};
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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;
}
Expand All @@ -138,7 +140,7 @@ export const elementToFormValues = function (element, fsm) {
return values;
};

const formCallableToElementCallable = function (
const formCallableToElementCallable = function(
callbleFormValue,
setFunc,
callablePbType,
Expand Down Expand Up @@ -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) {
Expand All @@ -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()) {
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -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(
Expand Down

0 comments on commit cdc2a1f

Please sign in to comment.