Added a new demo showing off flow diagram usage:

- PortModel can now decide whether or not a link should be allowed (E.g. only allowing outputs to connect to inputs)
- PortModel now has an optional maximum number of links - When set to 1 an existing link is returned by createLinkModel and when set to another finite number null will be returned when the maximum is reached
- LinkModel has been updated to support the resetting of existing links (I.e. removing ports and removing mid-points)
- DiagramWidget has been updated to handle null being returned by createLinkModel as well as an existing link (this also supports an existing link where the link's target port should now be the source port)
- DiagramWidget has been updated to respect the PortModel's new canLinkToPort method
- DiagramWidget has been updated to disallow duplicate links
This commit is contained in:
Andrew Young
2018-01-24 17:02:58 +00:00
parent 266eb85436
commit b7698ca53a
7 changed files with 154 additions and 15 deletions

View File

@ -99,12 +99,16 @@ export default () => {
model.addNode(node4); model.addNode(node4);
var link1 = node1.getOutPorts()[0].createLinkModel(); var link1 = node1.getOutPorts()[0].createLinkModel();
link1.setTargetPort(port3); if (link1) {
model.addLink(link1); link1.setTargetPort(port3);
model.addLink(link1);
}
var link2 = node1.getOutPorts()[1].createLinkModel(); var link2 = node1.getOutPorts()[1].createLinkModel();
link2.setTargetPort(port4); if (link2) {
model.addLink(link2); link2.setTargetPort(port4);
model.addLink(link2);
}
// load model into engine // load model into engine
engine.setDiagramModel(model); engine.setDiagramModel(model);

View File

@ -0,0 +1,53 @@
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
//2) setup the diagram model
var model = new DiagramModel();
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "OUT"));
node1.x = 100;
node1.y = 100;
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
//3-D) create an orphaned node
var node3 = new DefaultNodeModel("Node 3", "rgb(0,192,255)");
var port3 = node3.addPort(new DefaultPortModel(false, "out-1", "OUT"));
node3.x = 100;
node3.y = 200;
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addNode(node3);
model.addLink(link1);
//5) load model into engine
engine.setDiagramModel(model);
//6) render the diagram!
return <DiagramWidget diagramEngine={engine} allowLooseLinks={false} />;
};

View File

@ -35,6 +35,10 @@ storiesOf("Simple Usage", module)
require("./demo-simple/docs.md") require("./demo-simple/docs.md")
) )
) )
.add(
"Simple flow example",
Helper.makeDemo(require("./demo-simple-flow/index").default(), require("!!raw-loader!./demo-simple-flow/index"))
)
.add( .add(
"Performance demo", "Performance demo",
Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index")) Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index"))

View File

@ -9,7 +9,7 @@ export class DefaultPortModel extends PortModel {
label: string; label: string;
constructor(isInput: boolean, name: string, label: string = null, id?: string) { constructor(isInput: boolean, name: string, label: string = null, id?: string) {
super(name, "default", id); super(name, "default", id, isInput ? null : 1);
this.in = isInput; this.in = isInput;
this.label = label || name; this.label = label || name;
} }
@ -26,4 +26,8 @@ export class DefaultPortModel extends PortModel {
label: this.label label: this.label
}); });
} }
canLinkToPort(port: PortModel): boolean {
return port instanceof DefaultPortModel && this.in !== port.in;
}
} }

View File

@ -122,7 +122,13 @@ export class LinkModel extends BaseModel<LinkModelListener> {
} }
setSourcePort(port: PortModel) { setSourcePort(port: PortModel) {
port.addLink(this); if (port !== null) {
port.addLink(this);
} else if (this.sourcePort !== null) {
this.sourcePort.removeLink(this);
} else {
return;
}
this.sourcePort = port; this.sourcePort = port;
this.iterateListeners((listener: LinkModelListener, event) => { this.iterateListeners((listener: LinkModelListener, event) => {
listener.sourcePortChanged && listener.sourcePortChanged({ ...event, port: port }); listener.sourcePortChanged && listener.sourcePortChanged({ ...event, port: port });
@ -138,7 +144,13 @@ export class LinkModel extends BaseModel<LinkModelListener> {
} }
setTargetPort(port: PortModel) { setTargetPort(port: PortModel) {
port.addLink(this); if (port !== null) {
port.addLink(this);
} else if (this.targetPort !== null) {
this.targetPort.removeLink(this);
} else {
return;
}
this.targetPort = port; this.targetPort = port;
this.iterateListeners((listener: LinkModelListener, event) => { this.iterateListeners((listener: LinkModelListener, event) => {
listener.targetPortChanged && listener.targetPortChanged({ ...event, port: port }); listener.targetPortChanged && listener.targetPortChanged({ ...event, port: port });
@ -176,6 +188,12 @@ export class LinkModel extends BaseModel<LinkModelListener> {
this.points.splice(this.getPointIndex(pointModel) + 1); this.points.splice(this.getPointIndex(pointModel) + 1);
} }
removeMiddlePoints() {
if (this.points.length > 2) {
this.points.splice(0, this.points.length - 2);
}
}
addPoint(pointModel: PointModel, index = 1) { addPoint(pointModel: PointModel, index = 1) {
pointModel.link = this; pointModel.link = this;
this.points.splice(index, 0, pointModel); this.points.splice(index, 0, pointModel);

View File

@ -7,17 +7,20 @@ export class PortModel extends BaseModel<BaseModelListener> {
name: string; name: string;
parentNode: NodeModel; parentNode: NodeModel;
links: { [id: string]: LinkModel }; links: { [id: string]: LinkModel };
maximumLinks: number;
constructor(name: string, type?: string, id?: string) { constructor(name: string, type?: string, id?: string, maximumLinks?: number) {
super(type, id); super(type, id);
this.name = name; this.name = name;
this.links = {}; this.links = {};
this.parentNode = null; this.parentNode = null;
this.maximumLinks = maximumLinks;
} }
deSerialize(ob) { deSerialize(ob) {
super.deSerialize(ob); super.deSerialize(ob);
this.name = ob.name; this.name = ob.name;
this.maximumLinks = ob.maximumLinks;
} }
serialize() { serialize() {
@ -26,7 +29,8 @@ export class PortModel extends BaseModel<BaseModelListener> {
parentNode: this.parentNode.id, parentNode: this.parentNode.id,
links: _.map(this.links, link => { links: _.map(this.links, link => {
return link.id; return link.id;
}) }),
maximumLinks: this.maximumLinks
}); });
} }
@ -47,6 +51,14 @@ export class PortModel extends BaseModel<BaseModelListener> {
this.parentNode = node; this.parentNode = node;
} }
getMaximumLinks(): number {
return this.maximumLinks;
}
setMaximumLinks(maximumLinks: number) {
this.maximumLinks = maximumLinks;
}
removeLink(link: LinkModel) { removeLink(link: LinkModel) {
delete this.links[link.getID()]; delete this.links[link.getID()];
} }
@ -60,11 +72,24 @@ export class PortModel extends BaseModel<BaseModelListener> {
} }
createLinkModel(): LinkModel | null { createLinkModel(): LinkModel | null {
if (_.isFinite(this.maximumLinks)) {
var numberOfLinks: number = _.size(this.links);
if (this.maximumLinks === 1 && numberOfLinks >= 1) {
return _.values(this.links)[0];
} else if (numberOfLinks >= this.maximumLinks) {
return null;
}
}
var linkModel = new LinkModel(); var linkModel = new LinkModel();
linkModel.setSourcePort(this); linkModel.setSourcePort(this);
return linkModel; return linkModel;
} }
canLinkToPort(port: PortModel): boolean {
return true;
}
isLocked() { isLocked() {
return super.isLocked() || this.getParent().isLocked(); return super.isLocked() || this.getParent().isLocked();
} }

View File

@ -348,6 +348,29 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
} }
}); });
} }
//remove any invalid links
_.forEach(this.state.action.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
var link = model.model.getLink();
var sourcePort: PortModel = link.getSourcePort();
var targetPort: PortModel = link.getTargetPort();
if (sourcePort !== null && targetPort !== null) {
if (!sourcePort.canLinkToPort(targetPort)) {
//link not allowed
link.remove();
}
else if (_.some(_.values(targetPort.getLinks()), (l: LinkModel) => l !== link && (l.getSourcePort() === sourcePort || l.getTargetPort() === sourcePort))) {
//link is a duplicate
link.remove();
}
}
});
diagramEngine.clearRepaintEntities(); diagramEngine.clearRepaintEntities();
this.stopFiringAction(!this.state.wasMoved); this.stopFiringAction(!this.state.wasMoved);
} else { } else {
@ -454,14 +477,22 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
var sourcePort = model.model; var sourcePort = model.model;
var link = sourcePort.createLinkModel(); var link = sourcePort.createLinkModel();
link.getFirstPoint().updateLocation(relative); if (link) {
link.getLastPoint().updateLocation(relative); link.removeMiddlePoints();
if (link.getSourcePort() !== sourcePort) {
link.setSourcePort(sourcePort);
}
link.setTargetPort(null);
diagramModel.clearSelection(); link.getFirstPoint().updateLocation(relative);
link.getLastPoint().setSelected(true); link.getLastPoint().updateLocation(relative);
diagramModel.addLink(link);
this.startFiringAction(new MoveItemsAction(event.clientX, event.clientY, diagramEngine)); diagramModel.clearSelection();
link.getLastPoint().setSelected(true);
diagramModel.addLink(link);
this.startFiringAction(new MoveItemsAction(event.clientX, event.clientY, diagramEngine));
}
} else { } else {
diagramModel.clearSelection(); diagramModel.clearSelection();
} }