Compare commits

...

27 Commits

Author SHA1 Message Date
Dylan Vorster
2c730a9fd1 wip 2018-05-05 12:22:02 +02:00
Dylan Vorster
a086bc785e more wip 2018-05-02 11:31:18 +02:00
Dylan Vorster
a7b6012a50 improved generics 2018-05-02 11:23:56 +02:00
Dylan Vorster
4117af8a33 wip 2018-05-02 11:20:23 +02:00
Dylan Vorster
eb7f0642a5 more moving toward react-canvas 2018-05-01 19:23:46 +02:00
Dylan Vorster
a8c73115ac no longer need these actions 2018-05-01 12:15:20 +02:00
Dylan Vorster
404e12c4e8 more stuff we no longer need 2018-04-27 20:42:38 +02:00
Dylan Vorster
aa6d1c336c wip 2018-04-27 20:28:12 +02:00
Dylan Vorster
204e05a2a1 wip 2018-04-27 19:26:18 +02:00
Dylan Vorster
d12220baa0 more refactoring 2018-04-27 16:17:20 +02:00
Dylan Vorster
91c1ee0169 WIP 2018-04-27 15:54:28 +02:00
Dylan Vorster
4806805037 use base zoom and extend from more react-canvas 2018-04-27 15:24:04 +02:00
Dylan Vorster
22f4062f56 Move to react-canvas ;) 2018-04-27 15:08:40 +02:00
Dylan Vorster
32a3c33916 Merge pull request #230 from ganesh-sankey/master
on deserialization of diagram, labels are not getting rendered bug re…
2018-04-23 17:37:37 +02:00
Ganesh Kakade
75ef02dd4d on deserialization of diagram, labels are not getting rendered bug resolved 2018-04-23 20:44:24 +05:30
Dylan Vorster
665c8b3443 Update README.md 2018-04-15 16:36:51 +02:00
Dylan Vorster
ccf425676f Update README.md 2018-04-15 16:36:39 +02:00
Dylan Vorster
1ff3abf3ef Update README.md 2018-04-15 16:36:26 +02:00
Dylan Vorster
0b1dab0de6 Merge pull request #217 from yngndrw/allow-loose-links-fix-and-refactor
Allow loose links fix and refactor (Fix for #171)
2018-04-02 02:28:00 +02:00
Andrew Young
0988e625b1 Added E2E tests, but could not get them to run ? 2018-04-01 23:14:24 +01:00
Andrew Young
dbaf03662f Added @types directory to the .gitignore file 2018-04-01 20:45:19 +01:00
Andrew Young
55f62587bd Re-write the "allowLooseLinks" validation functionality - Now checks both end points on any selected links that have been moved, resulting in more reliable validation 2018-04-01 20:27:55 +01:00
Andrew Young
98bcd60396 Code consistency change 2018-04-01 20:26:41 +01:00
Andrew Young
8467d8e7ca Fixed a typo 2018-04-01 20:26:04 +01:00
Dylan Vorster
40b4e14f15 Merge pull request #207 from wader/always-remove-link-from-old-source-target-port
Always remove link from old source/target port on port change
2018-03-27 17:16:06 +02:00
Mattias Wadman
7a78aaa9fd Always remove link from old source/target port on port change
Was only removed from old if new port is null
2018-03-23 13:43:58 +01:00
Dylan Vorster
47214df76b Update README.md 2018-03-23 10:11:35 +02:00
68 changed files with 536 additions and 2630 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ dist/main.js
dist/main.js.map
/package
*.tgz
@types/
.out

View File

@@ -22,7 +22,7 @@ module.exports = {
},
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader',
loader: 'awesome-typescript-loader?declaration=false',
},
{
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,

1
.yarnrc Normal file
View File

@@ -0,0 +1 @@
--ignore-engines true

View File

@@ -1,5 +1,9 @@
# STORM React Diagrams
**PSA**: React Diagrams is currently getting a bit of a rewrite to enable much more advanced features. To see the new foundation WIP visit [https://github.com/projectstorm/react-canvas](https://github.com/projectstorm/react-canvas).
---
**DEMO**: [http://www.projectstorm.io/react-diagrams](http://www.projectstorm.io/react-diagrams)
**DOCS:** [https://projectstorm.gitbooks.io/react-diagrams](https://projectstorm.gitbooks.io/react-diagrams)
@@ -8,7 +12,7 @@
A super simple, no-nonsense diagramming library written in React that just works.
[![Join the chat at https://gitter.im/projectstorm/react-diagrams](https://badges.gitter.im/projectstorm/react-diagrams.svg)](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NPM](https://img.shields.io/npm/v/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams) [![NPM](https://img.shields.io/npm/dt/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams) ![](https://www.gitbook.com/button/status/book/projectstorm/react-diagrams) [![CircleCI](https://circleci.com/gh/projectstorm/react-diagrams/tree/master.svg?style=svg)](https://circleci.com/gh/projectstorm/react-diagrams/tree/master)
[![Join the chat at https://gitter.im/projectstorm/react-diagrams](https://badges.gitter.im/projectstorm/react-diagrams.svg)](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NPM](https://img.shields.io/npm/v/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams) [![NPM](https://img.shields.io/npm/dt/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams) [![Package Quality](http://npm.packagequality.com/shield/storm-react-diagrams.svg)](http://packagequality.com/#?package=storm-react-diagrams) [![CircleCI](https://circleci.com/gh/projectstorm/react-diagrams/tree/master.svg?style=svg)](https://circleci.com/gh/projectstorm/react-diagrams/tree/master)
![Personal Project](./images/example1.jpg)

View File

@@ -59,7 +59,7 @@ class CloneSelected extends React.Component<any, any> {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -81,7 +81,7 @@ export default () => {
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <CloneSelected engine={engine} model={model} />;

View File

@@ -118,7 +118,7 @@ export class AdvancedLinkFactory extends DefaultLinkFactory {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
engine.registerLinkFactory(new AdvancedLinkFactory());
// create some nodes
@@ -150,7 +150,7 @@ export default () => {
model.addAll(node1, node2, node3, node4);
// load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
// render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -20,7 +20,7 @@ import { DiamondPortModel } from "./DiamondPortModel";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
// register some other factories as well
engine.registerPortFactory(new SimplePortFactory("diamond", config => new DiamondPortModel()));
@@ -50,7 +50,7 @@ export default () => {
model.addAll(node1, node2, node3, link1, link2);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -40,7 +40,7 @@ class Demo8Widget extends React.Component<any, any> {
const { engine } = this.props;
const model = engine.getDiagramModel();
let distributedModel = getDistributedModel(engine, model);
engine.setDiagramModel(distributedModel);
engine.setModel(distributedModel);
this.forceUpdate();
}
@@ -68,7 +68,7 @@ function getDistributedModel(engine, model) {
export default () => {
//1) setup the diagram engine
let engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
let model = new DiagramModel();
@@ -114,7 +114,7 @@ export default () => {
//5) load model into engine
let model2 = getDistributedModel(engine, model);
engine.setDiagramModel(model2);
engine.setModel(model2);
return <Demo8Widget engine={engine} />;
};

View File

@@ -9,14 +9,14 @@ export class Application {
constructor() {
this.diagramEngine = new SRD.DiagramEngine();
this.diagramEngine.installDefaultFactories();
this.diagramEngine.installDefaults();
this.newModel();
}
public newModel() {
this.activeModel = new SRD.DiagramModel();
this.diagramEngine.setDiagramModel(this.activeModel);
this.diagramEngine.setModel(this.activeModel);
//3-A) create a default node
var node1 = new SRD.DefaultNodeModel("Node 1", "rgb(0,192,255)");

View File

@@ -7,7 +7,7 @@ import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -30,7 +30,7 @@ export default () => {
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -14,7 +14,7 @@ import { action } from "@storybook/addon-actions";
export default () => {
// setup the diagram engine
const engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
// setup the diagram model
const model = new DiagramModel();
@@ -53,7 +53,7 @@ export default () => {
model.addAll(node1, node2, node3, node4, link1, link2, link3);
// load model into engine and render
engine.setDiagramModel(model);
engine.setModel(model);
return (
<DemoWorkspaceWidget

View File

@@ -14,7 +14,7 @@ import {
export default () => {
// setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
var model = new DiagramModel();
@@ -33,7 +33,7 @@ export default () => {
model.addAll(node1, node2, link1);
engine.setDiagramModel(model);
engine.setModel(model);
var props = {
diagramEngine: engine,

View File

@@ -15,7 +15,7 @@ import {
export default () => {
// setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
var model = new DiagramModel();
@@ -46,7 +46,7 @@ export default () => {
});
});
engine.setDiagramModel(model);
engine.setModel(model);
var props = {
diagramEngine: engine,

View File

@@ -18,7 +18,7 @@ import {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
var model = new DiagramModel();
@@ -51,7 +51,7 @@ export default () => {
model.addAll(node3, node4, link2);
engine.setDiagramModel(model);
engine.setModel(model);
//!========================================= <<<<<<<

View File

@@ -31,7 +31,7 @@ class NodeDelayedPosition extends React.Component<any, any> {
node.x += 30;
node.y += 30;
model2.deSerializeDiagram(obj, engine);
engine.setDiagramModel(model2);
engine.setModel(model2);
this.forceUpdate();
}
@@ -57,7 +57,7 @@ class NodeDelayedPosition extends React.Component<any, any> {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -79,7 +79,7 @@ export default () => {
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <NodeDelayedPosition engine={engine} model={model} />;

View File

@@ -11,7 +11,7 @@ import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -23,7 +23,7 @@ export default () => {
}
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -1,13 +1,14 @@
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "storm-react-diagrams";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel } from "storm-react-diagrams";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
import { action } from "@storybook/addon-actions";
import * as beautify from "json-beautify";
import {DeserializeEvent} from "../../../react-canvas/src/models/BaseModel";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -29,17 +30,17 @@ export default () => {
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//!------------- SERIALIZING ------------------
var str = JSON.stringify(model.serializeDiagram());
var str = JSON.stringify(model.serialize());
//!------------- DESERIALIZING ----------------
var model2 = new DiagramModel();
model2.deSerializeDiagram(JSON.parse(str), engine);
engine.setDiagramModel(model2);
model2.deSerialize(new DeserializeEvent(JSON.parse(str), engine));
engine.setModel(model2);
return (
<DemoWorkspaceWidget

View File

@@ -4,7 +4,7 @@ import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -31,7 +31,7 @@ export default () => {
model.addAll(node1, node2, node3, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} allowLooseLinks={false} />;

View File

@@ -11,7 +11,7 @@ import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -34,7 +34,7 @@ export default () => {
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -13,7 +13,7 @@ import { action } from "@storybook/addon-actions";
export default () => {
// setup the diagram engine
const engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
// setup the diagram model
const model = new DiagramModel();
@@ -43,7 +43,7 @@ export default () => {
model.addAll(node1, node2, node3, node4, node5, link1, link2);
// load model into engine and render
engine.setDiagramModel(model);
engine.setModel(model);
return (
<DemoWorkspaceWidget

View File

@@ -18,7 +18,7 @@ import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -30,11 +30,13 @@ export default () => {
}
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return (
<DemoWorkspaceWidget buttons={<button onClick={() => engine.zoomToFit()}>Zoom to fit</button>}>
<DemoWorkspaceWidget
buttons={<button onClick={() => engine.getCanvasWidget().zoomToFit()}>Zoom to fit</button>}
>
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);

View File

@@ -34,96 +34,96 @@ storiesOf("Simple Usage", module)
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(
"Performance demo",
Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index"))
)
.add(
"Locked widget",
Helper.makeDemo(require("./demo-locks/index").default(), require("!!raw-loader!./demo-locks/index"))
)
.add(
"Canvas grid size",
Helper.makeDemo(require("./demo-grid/index").default(), require("!!raw-loader!./demo-grid/index"))
)
.add(
"Limiting link points",
Helper.makeDemo(
require("./demo-limit-points/index").default(),
require("!!raw-loader!./demo-limit-points/index")
)
)
.add(
"Events and listeners",
Helper.makeDemo(require("./demo-listeners/index").default(), require("!!raw-loader!./demo-listeners/index"))
)
.add(
"Zoom to fit",
Helper.makeDemo(require("./demo-zoom-to-fit/index").default(), require("!!raw-loader!./demo-zoom-to-fit/index"))
)
.add(
"Links with labels",
Helper.makeDemo(
require("./demo-labelled-links/index").default(),
require("!!raw-loader!./demo-labelled-links/index")
)
);
storiesOf("Advanced Techniques", module)
.add(
"Clone Selected",
Helper.makeDemo(require("./demo-cloning/index").default(), require("!!raw-loader!./demo-cloning/index"))
)
.add(
"Serializing and de-serializing",
Helper.makeDemo(require("./demo-serializing/index").default(), require("!!raw-loader!./demo-serializing/index"))
)
.add(
"Programatically modifying graph",
Helper.makeDemo(
require("./demo-mutate-graph/index").default(),
require("!!raw-loader!./demo-mutate-graph/index")
)
)
.add(
"Large application",
Helper.makeDemo(
require("./demo-drag-and-drop/index").default(),
require("!!raw-loader!./demo-drag-and-drop/components/BodyWidget")
)
)
.add(
"Smart routing",
Helper.makeDemo(
require("./demo-smart-routing/index").default(),
require("!!raw-loader!./demo-smart-routing/index")
)
);
storiesOf("Custom Models", module)
.add(
"Custom diamond node",
Helper.makeDemo(
require("./demo-custom-node1/index").default(),
require("!!raw-loader!./demo-custom-node1/index")
)
)
.add(
"Custom animated links",
Helper.makeDemo(
require("./demo-custom-link1/index").default(),
require("!!raw-loader!./demo-custom-link1/index")
)
);
storiesOf("3rd party libraries", module).add(
"Auto Distribute (Dagre)",
Helper.makeDemo(require("./demo-dagre/index").default(), require("!!raw-loader!./demo-dagre/index"))
);
// .add(
// "Simple flow example",
// Helper.makeDemo(require("./demo-simple-flow/index").default(), require("!!raw-loader!./demo-simple-flow/index"))
// )
// .add(
// "Performance demo",
// Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index"))
// )
// .add(
// "Locked widget",
// Helper.makeDemo(require("./demo-locks/index").default(), require("!!raw-loader!./demo-locks/index"))
// )
// .add(
// "Canvas grid size",
// Helper.makeDemo(require("./demo-grid/index").default(), require("!!raw-loader!./demo-grid/index"))
// )
// .add(
// "Limiting link points",
// Helper.makeDemo(
// require("./demo-limit-points/index").default(),
// require("!!raw-loader!./demo-limit-points/index")
// )
// )
// .add(
// "Events and listeners",
// Helper.makeDemo(require("./demo-listeners/index").default(), require("!!raw-loader!./demo-listeners/index"))
// )
// .add(
// "Zoom to fit",
// Helper.makeDemo(require("./demo-zoom-to-fit/index").default(), require("!!raw-loader!./demo-zoom-to-fit/index"))
// )
// .add(
// "Links with labels",
// Helper.makeDemo(
// require("./demo-labelled-links/index").default(),
// require("!!raw-loader!./demo-labelled-links/index")
// )
// );
//
// storiesOf("Advanced Techniques", module)
// .add(
// "Clone Selected",
// Helper.makeDemo(require("./demo-cloning/index").default(), require("!!raw-loader!./demo-cloning/index"))
// )
// .add(
// "Serializing and de-serializing",
// Helper.makeDemo(require("./demo-serializing/index").default(), require("!!raw-loader!./demo-serializing/index"))
// )
// .add(
// "Programatically modifying graph",
// Helper.makeDemo(
// require("./demo-mutate-graph/index").default(),
// require("!!raw-loader!./demo-mutate-graph/index")
// )
// )
// .add(
// "Large application",
// Helper.makeDemo(
// require("./demo-drag-and-drop/index").default(),
// require("!!raw-loader!./demo-drag-and-drop/components/BodyWidget")
// )
// )
// .add(
// "Smart routing",
// Helper.makeDemo(
// require("./demo-smart-routing/index").default(),
// require("!!raw-loader!./demo-smart-routing/index")
// )
// );
//
// storiesOf("Custom Models", module)
// .add(
// "Custom diamond node",
// Helper.makeDemo(
// require("./demo-custom-node1/index").default(),
// require("!!raw-loader!./demo-custom-node1/index")
// )
// )
// .add(
// "Custom animated links",
// Helper.makeDemo(
// require("./demo-custom-link1/index").default(),
// require("!!raw-loader!./demo-custom-link1/index")
// )
// );
//
// storiesOf("3rd party libraries", module).add(
// "Auto Distribute (Dagre)",
// Helper.makeDemo(require("./demo-dagre/index").default(), require("!!raw-loader!./demo-dagre/index"))
// );
// enable this to log mouse location when writing new puppeteer tests
//Helper.logMousePosition()

View File

@@ -1,115 +0,0 @@
import { Toolkit } from "./Toolkit";
import * as _ from "lodash";
import { DiagramEngine } from "./DiagramEngine";
/**
* @author Dylan Vorster
*/
export interface BaseEvent<T extends BaseEntity = any> {
entity: BaseEntity<BaseListener>;
stopPropagation: () => any;
firing: boolean;
id: string;
}
export interface BaseListener<T extends BaseEntity = any> {
lockChanged?(event: BaseEvent<T> & { locked: boolean }): void;
}
export type BaseEntityType = "node" | "link" | "port" | "point";
export class BaseEntity<T extends BaseListener = BaseListener> {
public listeners: { [s: string]: T };
public id: string;
public locked: boolean;
constructor(id?: string) {
this.listeners = {};
this.id = id || Toolkit.UID();
this.locked = false;
}
getID() {
return this.id;
}
doClone(lookupTable: { [s: string]: any } = {}, clone: any) {
/*noop*/
}
clone(lookupTable: { [s: string]: any } = {}) {
// try and use an existing clone first
if (lookupTable[this.id]) {
return lookupTable[this.id];
}
let clone = _.clone(this);
clone.id = Toolkit.UID();
clone.clearListeners();
lookupTable[this.id] = clone;
this.doClone(lookupTable, clone);
return clone;
}
clearListeners() {
this.listeners = {};
}
public deSerialize(data: { [s: string]: any }, engine: DiagramEngine) {
this.id = data.id;
}
public serialize() {
return {
id: this.id
};
}
public iterateListeners(cb: (t: T, event: BaseEvent) => any) {
let event: BaseEvent = {
id: Toolkit.UID(),
firing: true,
entity: this,
stopPropagation: () => {
event.firing = false;
}
};
for (var i in this.listeners) {
if (this.listeners.hasOwnProperty(i)) {
// propagation stopped
if (!event.firing) {
return;
}
cb(this.listeners[i], event);
}
}
}
public removeListener(listener: string) {
if (this.listeners[listener]) {
delete this.listeners[listener];
return true;
}
return false;
}
public addListener(listener: T): string {
var uid = Toolkit.UID();
this.listeners[uid] = listener;
return uid;
}
public isLocked(): boolean {
return this.locked;
}
public setLocked(locked: boolean = true) {
this.locked = locked;
this.iterateListeners((listener, event) => {
if (listener.lockChanged) {
listener.lockChanged({ ...event, locked: locked });
}
});
}
}

View File

@@ -1,365 +1,26 @@
import { BaseEntity, BaseListener } from "./BaseEntity";
import { DiagramModel } from "./models/DiagramModel";
import * as _ from "lodash";
import { BaseModel, BaseModelListener } from "./models/BaseModel";
import { NodeModel } from "./models/NodeModel";
import { PointModel } from "./models/PointModel";
import { PortModel } from "./models/PortModel";
import { LinkModel } from "./models/LinkModel";
import { AbstractLabelFactory } from "./factories/AbstractLabelFactory";
import { AbstractLinkFactory } from "./factories/AbstractLinkFactory";
import { AbstractNodeFactory } from "./factories/AbstractNodeFactory";
import { AbstractPortFactory } from "./factories/AbstractPortFactory";
import { DefaultLinkFactory, DefaultNodeFactory } from "./main";
import { ROUTING_SCALING_FACTOR } from "./routing/PathFinding";
import { DefaultPortFactory } from "./defaults/factories/DefaultPortFactory";
import { LabelModel } from "./models/LabelModel";
import { DefaultLabelFactory } from "./defaults/factories/DefaultLabelFactory";
import { Toolkit } from "./Toolkit";
/**
* @author Dylan Vorster
*/
export interface DiagramEngineListener extends BaseListener {
portFactoriesUpdated?(): void;
import { CanvasEngine } from "@projectstorm/react-canvas";
import { DefaultLabelFactory, DefaultLinkFactory, DefaultNodeFactory, DefaultPortFactory } from "storm-react-diagrams";
nodeFactoriesUpdated?(): void;
linkFactoriesUpdated?(): void;
labelFactoriesUpdated?(): void;
repaintCanvas?(): void;
}
/**
* Passed as a parameter to the DiagramWidget
*/
export class DiagramEngine extends BaseEntity<DiagramEngineListener> {
nodeFactories: { [s: string]: AbstractNodeFactory };
linkFactories: { [s: string]: AbstractLinkFactory };
portFactories: { [s: string]: AbstractPortFactory };
labelFactories: { [s: string]: AbstractLabelFactory };
diagramModel: DiagramModel;
canvas: Element;
export class DiagramEngine extends CanvasEngine<DiagramModel> {
paintableWidgets: {};
linksThatHaveInitiallyRendered: {};
nodesRendered: boolean;
maxNumberPointsPerLink: number;
smartRouting: boolean;
// calculated only when smart routing is active
canvasMatrix: number[][] = [];
routingMatrix: number[][] = [];
// used when at least one element has negative coordinates
hAdjustmentFactor: number = 0;
vAdjustmentFactor: number = 0;
constructor() {
super();
this.diagramModel = new DiagramModel();
this.nodeFactories = {};
this.linkFactories = {};
this.portFactories = {};
this.labelFactories = {};
this.canvas = null;
this.paintableWidgets = null;
this.linksThatHaveInitiallyRendered = {};
if (Toolkit.TESTING) {
Toolkit.TESTING_UID = 0;
//pop it onto the window so our E2E helpers can find it
if (window) {
(window as any)["diagram_instance"] = this;
}
}
this.smartRouting = false;
}
installDefaultFactories() {
this.registerNodeFactory(new DefaultNodeFactory());
this.registerLinkFactory(new DefaultLinkFactory());
this.registerPortFactory(new DefaultPortFactory());
this.registerLabelFactory(new DefaultLabelFactory());
}
repaintCanvas() {
this.iterateListeners(listener => {
if (listener.repaintCanvas) {
listener.repaintCanvas();
}
});
}
clearRepaintEntities() {
this.paintableWidgets = null;
}
enableRepaintEntities(entities: BaseModel<BaseEntity, BaseModelListener>[]) {
this.paintableWidgets = {};
entities.forEach(entity => {
//if a node is requested to repaint, add all of its links
if (entity instanceof NodeModel) {
_.forEach(entity.getPorts(), port => {
_.forEach(port.getLinks(), link => {
this.paintableWidgets[link.getID()] = true;
});
});
}
if (entity instanceof PointModel) {
this.paintableWidgets[entity.getLink().getID()] = true;
}
this.paintableWidgets[entity.getID()] = true;
});
}
/**
* Checks to see if a model is locked by running through
* its parents to see if they are locked first
*/
isModelLocked(model: BaseEntity<BaseListener>) {
//always check the diagram model
if (this.diagramModel.isLocked()) {
return true;
}
return model.isLocked();
}
recalculatePortsVisually() {
this.nodesRendered = false;
this.linksThatHaveInitiallyRendered = {};
}
canEntityRepaint(baseModel: BaseModel<BaseEntity, BaseModelListener>) {
//no rules applied, allow repaint
if (this.paintableWidgets === null) {
return true;
}
return this.paintableWidgets[baseModel.getID()] !== undefined;
}
setCanvas(canvas: Element | null) {
this.canvas = canvas;
}
setDiagramModel(model: DiagramModel) {
this.diagramModel = model;
this.recalculatePortsVisually();
}
getDiagramModel(): DiagramModel {
return this.diagramModel;
}
//!-------------- FACTORIES ------------
getNodeFactories(): { [s: string]: AbstractNodeFactory } {
return this.nodeFactories;
}
getLinkFactories(): { [s: string]: AbstractLinkFactory } {
return this.linkFactories;
}
getLabelFactories(): { [s: string]: AbstractLabelFactory } {
return this.labelFactories;
}
registerLabelFactory(factory: AbstractLabelFactory) {
this.labelFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.labelFactoriesUpdated) {
listener.labelFactoriesUpdated();
}
});
}
registerPortFactory(factory: AbstractPortFactory) {
this.portFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.portFactoriesUpdated) {
listener.portFactoriesUpdated();
}
});
}
registerNodeFactory(factory: AbstractNodeFactory) {
this.nodeFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.nodeFactoriesUpdated) {
listener.nodeFactoriesUpdated();
}
});
}
registerLinkFactory(factory: AbstractLinkFactory) {
this.linkFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.linkFactoriesUpdated) {
listener.linkFactoriesUpdated();
}
});
}
getPortFactory(type: string): AbstractPortFactory {
if (this.portFactories[type]) {
return this.portFactories[type];
}
throw new Error(`cannot find factory for port of type: [${type}]`);
}
getNodeFactory(type: string): AbstractNodeFactory {
if (this.nodeFactories[type]) {
return this.nodeFactories[type];
}
throw new Error(`cannot find factory for node of type: [${type}]`);
}
getLinkFactory(type: string): AbstractLinkFactory {
if (this.linkFactories[type]) {
return this.linkFactories[type];
}
throw new Error(`cannot find factory for link of type: [${type}]`);
}
getLabelFactory(type: string): AbstractLabelFactory {
if (this.labelFactories[type]) {
return this.labelFactories[type];
}
throw new Error(`cannot find factory for label of type: [${type}]`);
}
getFactoryForNode(node: NodeModel): AbstractNodeFactory | null {
return this.getNodeFactory(node.getType());
}
getFactoryForLink(link: LinkModel): AbstractLinkFactory | null {
return this.getLinkFactory(link.getType());
}
getFactoryForLabel(label: LabelModel): AbstractLabelFactory | null {
return this.getLabelFactory(label.getType());
}
generateWidgetForLink(link: LinkModel): JSX.Element | null {
var linkFactory = this.getFactoryForLink(link);
if (!linkFactory) {
throw new Error("Cannot find link factory for link: " + link.getType());
}
return linkFactory.generateReactWidget(this, link);
}
generateWidgetForNode(node: NodeModel): JSX.Element | null {
var nodeFactory = this.getFactoryForNode(node);
if (!nodeFactory) {
throw new Error("Cannot find widget factory for node: " + node.getType());
}
return nodeFactory.generateReactWidget(this, node);
}
getRelativeMousePoint(event): { x: number; y: number } {
var point = this.getRelativePoint(event.clientX, event.clientY);
return {
x: (point.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
y: (point.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
};
}
getRelativePoint(x, y) {
var canvasRect = this.canvas.getBoundingClientRect();
return { x: x - canvasRect.left, y: y - canvasRect.top };
}
getNodeElement(node: NodeModel): Element {
const selector = this.canvas.querySelector(`.node[data-nodeid="${node.getID()}"]`);
if (selector === null) {
throw new Error("Cannot find Node element with nodeID: [" + node.getID() + "]");
}
return selector;
}
getNodePortElement(port: PortModel): any {
var selector = this.canvas.querySelector(
`.port[data-name="${port.getName()}"][data-nodeid="${port.getParent().getID()}"]`
);
if (selector === null) {
throw new Error(
"Cannot find Node Port element with nodeID: [" +
port.getParent().getID() +
"] and name: [" +
port.getName() +
"]"
);
}
return selector;
}
getPortCenter(port: PortModel) {
var sourceElement = this.getNodePortElement(port);
var sourceRect = sourceElement.getBoundingClientRect();
var rel = this.getRelativePoint(sourceRect.left, sourceRect.top);
return {
x:
sourceElement.offsetWidth / 2 +
(rel.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
y:
sourceElement.offsetHeight / 2 +
(rel.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
};
}
/**
* Calculate rectangular coordinates of the port passed in.
*/
getPortCoords(
port: PortModel
): {
x: number;
y: number;
width: number;
height: number;
} {
const sourceElement = this.getNodePortElement(port);
const sourceRect = sourceElement.getBoundingClientRect();
const canvasRect = this.canvas.getBoundingClientRect() as ClientRect;
return {
x:
(sourceRect.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0) -
canvasRect.left,
y:
(sourceRect.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0) -
canvasRect.top,
width: sourceRect.width,
height: sourceRect.height
};
}
/**
* Determine the width and height of the node passed in.
* It currently assumes nodes have a rectangular shape, can be overriden for customised shapes.
*/
getNodeDimensions(node: NodeModel): { width: number; height: number } {
if (!this.canvas) {
return {
width: 0,
height: 0
};
}
const nodeElement = this.getNodeElement(node);
const nodeRect = nodeElement.getBoundingClientRect();
return {
width: nodeRect.width,
height: nodeRect.height
};
installDefaults() {
super.installDefaults();
this.registerElementFactory(new DefaultLabelFactory());
this.registerElementFactory(new DefaultLinkFactory());
this.registerElementFactory(new DefaultNodeFactory());
this.registerElementFactory(new DefaultPortFactory());
}
getMaxNumberPointsPerLink(): number {
@@ -371,213 +32,10 @@ export class DiagramEngine extends BaseEntity<DiagramEngineListener> {
}
isSmartRoutingEnabled() {
return !!this.smartRouting;
return this.smartRouting;
}
setSmartRoutingStatus(status: boolean) {
this.smartRouting = status;
}
/**
* A representation of the canvas in the following format:
*
* +-----------------+
* | 0 0 0 0 0 0 0 0 |
* | 0 0 0 0 0 0 0 0 |
* | 0 0 0 0 0 0 0 0 |
* | 0 0 0 0 0 0 0 0 |
* | 0 0 0 0 0 0 0 0 |
* +-----------------+
*
* In which all walkable points are marked by zeros.
* It uses @link{#ROUTING_SCALING_FACTOR} to reduce the matrix dimensions and improve performance.
*/
getCanvasMatrix(): number[][] {
if (this.canvasMatrix.length === 0) {
this.calculateCanvasMatrix();
}
return this.canvasMatrix;
}
calculateCanvasMatrix() {
const {
width: canvasWidth,
hAdjustmentFactor,
height: canvasHeight,
vAdjustmentFactor
} = this.calculateMatrixDimensions();
this.hAdjustmentFactor = hAdjustmentFactor;
this.vAdjustmentFactor = vAdjustmentFactor;
const matrixWidth = Math.ceil(canvasWidth / ROUTING_SCALING_FACTOR);
const matrixHeight = Math.ceil(canvasHeight / ROUTING_SCALING_FACTOR);
this.canvasMatrix = _.range(0, matrixHeight).map(() => {
return new Array(matrixWidth).fill(0);
});
}
/**
* A representation of the canvas in the following format:
*
* +-----------------+
* | 0 0 1 1 0 0 0 0 |
* | 0 0 1 1 0 0 1 1 |
* | 0 0 0 0 0 0 1 1 |
* | 1 1 0 0 0 0 0 0 |
* | 1 1 0 0 0 0 0 0 |
* +-----------------+
*
* In which all points blocked by a node (and its ports) are
* marked as 1; points were there is nothing (ie, free) receive 0.
*/
getRoutingMatrix(): number[][] {
if (this.routingMatrix.length === 0) {
this.calculateRoutingMatrix();
}
return this.routingMatrix;
}
calculateRoutingMatrix(): void {
const matrix = _.cloneDeep(this.getCanvasMatrix());
// nodes need to be marked as blocked points
this.markNodes(matrix);
// same thing for ports
this.markPorts(matrix);
this.routingMatrix = matrix;
}
/**
* The routing matrix does not have negative indexes, but elements could be negatively positioned.
* We use the functions below to translate back and forth between these coordinates, relying on the
* calculated values of hAdjustmentFactor and vAdjustmentFactor.
*/
translateRoutingX(x: number, reverse: boolean = false) {
return x + this.hAdjustmentFactor * (reverse ? -1 : 1);
}
translateRoutingY(y: number, reverse: boolean = false) {
return y + this.vAdjustmentFactor * (reverse ? -1 : 1);
}
/**
* Despite being a long method, we simply iterate over all three collections (nodes, ports and points)
* to find the highest X and Y dimensions, so we can build the matrix large enough to contain all elements.
*/
calculateMatrixDimensions = (): {
width: number;
hAdjustmentFactor: number;
height: number;
vAdjustmentFactor: number;
} => {
const allNodesCoords = _.values(this.diagramModel.nodes).map(item => ({
x: item.x,
width: item.width,
y: item.y,
height: item.height
}));
const allLinks = _.values(this.diagramModel.links);
const allPortsCoords = _.flatMap(allLinks.map(link => [link.sourcePort, link.targetPort]))
.filter(port => port !== null)
.map(item => ({
x: item.x,
width: item.width,
y: item.y,
height: item.height
}));
const allPointsCoords = _.flatMap(allLinks.map(link => link.points)).map(item => ({
// points don't have width/height, so let's just use 0
x: item.x,
width: 0,
y: item.y,
height: 0
}));
const canvas = this.canvas as HTMLDivElement;
const minX =
Math.floor(
Math.min(_.minBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.x).x, 0) /
ROUTING_SCALING_FACTOR
) * ROUTING_SCALING_FACTOR;
const maxXElement = _.maxBy(
_.concat(allNodesCoords, allPortsCoords, allPointsCoords),
item => item.x + item.width
);
const maxX = Math.max(maxXElement.x + maxXElement.width, canvas.offsetWidth);
const minY =
Math.floor(
Math.min(_.minBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.y).y, 0) /
ROUTING_SCALING_FACTOR
) * ROUTING_SCALING_FACTOR;
const maxYElement = _.maxBy(
_.concat(allNodesCoords, allPortsCoords, allPointsCoords),
item => item.y + item.height
);
const maxY = Math.max(maxYElement.y + maxYElement.height, canvas.offsetHeight);
return {
width: Math.ceil(Math.abs(minX) + maxX),
hAdjustmentFactor: Math.abs(minX) / ROUTING_SCALING_FACTOR + 1,
height: Math.ceil(Math.abs(minY) + maxY),
vAdjustmentFactor: Math.abs(minY) / ROUTING_SCALING_FACTOR + 1
};
};
/**
* Updates (by reference) where nodes will be drawn on the matrix passed in.
*/
markNodes = (matrix: number[][]): void => {
_.values(this.diagramModel.nodes).forEach(node => {
const startX = Math.floor(node.x / ROUTING_SCALING_FACTOR);
const endX = Math.ceil((node.x + node.width) / ROUTING_SCALING_FACTOR);
const startY = Math.floor(node.y / ROUTING_SCALING_FACTOR);
const endY = Math.ceil((node.y + node.height) / ROUTING_SCALING_FACTOR);
for (let x = startX - 1; x <= endX + 1; x++) {
for (let y = startY - 1; y < endY + 1; y++) {
this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y));
}
}
});
};
/**
* Updates (by reference) where ports will be drawn on the matrix passed in.
*/
markPorts = (matrix: number[][]): void => {
const allElements = _.flatMap(
_.values(this.diagramModel.links).map(link => [].concat(link.sourcePort, link.targetPort))
);
allElements.filter(port => port !== null).forEach(port => {
const startX = Math.floor(port.x / ROUTING_SCALING_FACTOR);
const endX = Math.ceil((port.x + port.width) / ROUTING_SCALING_FACTOR);
const startY = Math.floor(port.y / ROUTING_SCALING_FACTOR);
const endY = Math.ceil((port.y + port.height) / ROUTING_SCALING_FACTOR);
for (let x = startX - 1; x <= endX + 1; x++) {
for (let y = startY - 1; y < endY + 1; y++) {
this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y));
}
}
});
};
markMatrixPoint = (matrix: number[][], x: number, y: number) => {
if (matrix[y] !== undefined && matrix[y][x] !== undefined) {
matrix[y][x] = 1;
}
};
zoomToFit() {
const xFactor = this.canvas.clientWidth / this.canvas.scrollWidth;
const yFactor = this.canvas.clientHeight / this.canvas.scrollHeight;
const zoomFactor = xFactor < yFactor ? xFactor : yFactor;
this.diagramModel.setZoomLevel(this.diagramModel.getZoomLevel() * zoomFactor);
this.diagramModel.setOffset(0, 0);
this.repaintCanvas();
}
}

View File

@@ -3,29 +3,11 @@ import closest = require("closest");
import { PointModel } from "./models/PointModel";
import { ROUTING_SCALING_FACTOR } from "./routing/PathFinding";
import * as Path from "paths-js/path";
import { Toolkit as TK } from "@projectstorm/react-canvas";
/**
* @author Dylan Vorster
*/
export class Toolkit {
static TESTING: boolean = false;
static TESTING_UID = 0;
/**
* Generats a unique ID (thanks Stack overflow :3)
* @returns {String}
*/
public static UID(): string {
if (Toolkit.TESTING) {
Toolkit.TESTING_UID++;
return "" + Toolkit.TESTING_UID;
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export class Toolkit extends TK {
/**
* Finds the closest element as a polyfill
*

View File

@@ -1,11 +0,0 @@
export class BaseAction {
mouseX: number;
mouseY: number;
ms: number;
constructor(mouseX: number, mouseY: number) {
this.mouseX = mouseX;
this.mouseY = mouseY;
this.ms = new Date().getTime();
}
}

View File

@@ -1,13 +0,0 @@
import { BaseAction } from "./BaseAction";
import { DiagramModel } from "../models/DiagramModel";
export class MoveCanvasAction extends BaseAction {
initialOffsetX: number;
initialOffsetY: number;
constructor(mouseX: number, mouseY: number, diagramModel: DiagramModel) {
super(mouseX, mouseY);
this.initialOffsetX = diagramModel.getOffsetX();
this.initialOffsetY = diagramModel.getOffsetY();
}
}

View File

@@ -1,30 +0,0 @@
import { BaseAction } from "./BaseAction";
import { SelectionModel } from "../models/SelectionModel";
import { PointModel } from "../models/PointModel";
import { NodeModel } from "../models/NodeModel";
import { DiagramEngine } from "../DiagramEngine";
export class MoveItemsAction extends BaseAction {
selectionModels: SelectionModel[];
moved: boolean;
constructor(mouseX: number, mouseY: number, diagramEngine: DiagramEngine) {
super(mouseX, mouseY);
this.moved = false;
diagramEngine.enableRepaintEntities(diagramEngine.getDiagramModel().getSelectedItems());
var selectedItems = diagramEngine.getDiagramModel().getSelectedItems();
//dont allow items which are locked to move
selectedItems = selectedItems.filter(item => {
return !diagramEngine.isModelLocked(item);
});
this.selectionModels = selectedItems.map((item: PointModel | NodeModel) => {
return {
model: item,
initialX: item.x,
initialY: item.y
};
});
}
}

View File

@@ -1,36 +0,0 @@
import { BaseAction } from "./BaseAction";
import { DiagramModel } from "../models/DiagramModel";
export class SelectingAction extends BaseAction {
mouseX2: number;
mouseY2: number;
constructor(mouseX: number, mouseY: number) {
super(mouseX, mouseY);
this.mouseX2 = mouseX;
this.mouseY2 = mouseY;
}
getBoxDimensions() {
return {
left: this.mouseX2 > this.mouseX ? this.mouseX : this.mouseX2,
top: this.mouseY2 > this.mouseY ? this.mouseY : this.mouseY2,
width: Math.abs(this.mouseX2 - this.mouseX),
height: Math.abs(this.mouseY2 - this.mouseY),
right: this.mouseX2 < this.mouseX ? this.mouseX : this.mouseX2,
bottom: this.mouseY2 < this.mouseY ? this.mouseY : this.mouseY2
};
}
containsElement(x: number, y: number, diagramModel: DiagramModel): boolean {
var z = diagramModel.getZoomLevel() / 100.0;
let dimensions = this.getBoxDimensions();
return (
x * z + diagramModel.getOffsetX() > dimensions.left &&
x * z + diagramModel.getOffsetX() < dimensions.right &&
y * z + diagramModel.getOffsetY() > dimensions.top &&
y * z + diagramModel.getOffsetY() < dimensions.bottom
);
}
}

View File

@@ -1,22 +1,19 @@
import * as React from "react";
import { DiagramEngine } from "../../DiagramEngine";
import { AbstractLabelFactory } from "../../factories/AbstractLabelFactory";
import { DefaultLabelModel } from "../models/DefaultLabelModel";
import { DefaultLabelWidget } from "../widgets/DefaultLabelWidget";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
/**
* @author Dylan Vorster
*/
export class DefaultLabelFactory extends AbstractLabelFactory<DefaultLabelModel> {
export class DefaultLabelFactory extends AbstractElementFactory<DefaultLabelModel> {
constructor() {
super("default");
}
generateReactWidget(diagramEngine: DiagramEngine, label: DefaultLabelModel): JSX.Element {
return <DefaultLabelWidget model={label} />;
generateWidget(engine: DiagramEngine, model: DefaultLabelModel): JSX.Element {
return <DefaultLabelWidget model={model} />;
}
getNewInstance(initialConfig?: any): DefaultLabelModel {
generateModel(): DefaultLabelModel {
return new DefaultLabelModel();
}
}

View File

@@ -1,25 +1,22 @@
import * as React from "react";
import { DefaultLinkWidget } from "../widgets/DefaultLinkWidget";
import { DiagramEngine } from "../../DiagramEngine";
import { AbstractLinkFactory } from "../../factories/AbstractLinkFactory";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
import { DefaultLinkModel } from "../models/DefaultLinkModel";
/**
* @author Dylan Vorster
*/
export class DefaultLinkFactory extends AbstractLinkFactory<DefaultLinkModel> {
export class DefaultLinkFactory extends AbstractElementFactory<DefaultLinkModel> {
constructor() {
super("default");
}
generateReactWidget(diagramEngine: DiagramEngine, link: DefaultLinkModel): JSX.Element {
generateWidget(engine: DiagramEngine, model: DefaultLinkModel): JSX.Element {
return React.createElement(DefaultLinkWidget, {
link: link,
diagramEngine: diagramEngine
link: model,
diagramEngine: engine
});
}
getNewInstance(initialConfig?: any): DefaultLinkModel {
generateModel(): DefaultLinkModel {
return new DefaultLinkModel();
}
@@ -27,8 +24,8 @@ export class DefaultLinkFactory extends AbstractLinkFactory<DefaultLinkModel> {
return (
<path
className={selected ? widget.bem("--path-selected") : ""}
strokeWidth={model.width}
stroke={model.color}
strokeWidth={model.getWidth()}
stroke={model.getColor()}
d={path}
/>
);

View File

@@ -2,23 +2,21 @@ import { DefaultNodeModel } from "../models/DefaultNodeModel";
import * as React from "react";
import { DefaultNodeWidget } from "../widgets/DefaultNodeWidget";
import { DiagramEngine } from "../../DiagramEngine";
import { AbstractNodeFactory } from "../../factories/AbstractNodeFactory";
/**
* @author Dylan Vorster
*/
export class DefaultNodeFactory extends AbstractNodeFactory<DefaultNodeModel> {
import { AbstractElementFactory } from "@projectstorm/react-canvas";
export class DefaultNodeFactory extends AbstractElementFactory<DefaultNodeModel> {
constructor() {
super("default");
}
generateReactWidget(diagramEngine: DiagramEngine, node: DefaultNodeModel): JSX.Element {
generateWidget(diagramEngine: DiagramEngine, model: DefaultNodeModel): JSX.Element {
return React.createElement(DefaultNodeWidget, {
node: node,
node: model,
diagramEngine: diagramEngine
});
}
getNewInstance(initialConfig?: any): DefaultNodeModel {
generateModel(): DefaultNodeModel {
return new DefaultNodeModel();
}
}

View File

@@ -1,12 +1,17 @@
import { DefaultPortModel } from "../models/DefaultPortModel";
import { AbstractPortFactory } from "../../factories/AbstractPortFactory";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
import { DiagramEngine } from "storm-react-diagrams";
export class DefaultPortFactory extends AbstractPortFactory<DefaultPortModel> {
export class DefaultPortFactory extends AbstractElementFactory<DefaultPortModel> {
constructor() {
super("default");
}
getNewInstance(initialConfig?: any): DefaultPortModel {
generateWidget(engine: DiagramEngine, model: DefaultPortModel): JSX.Element {
return null;
}
generateModel(): DefaultPortModel {
return new DefaultPortModel(true, "unknown");
}
}

View File

@@ -1,7 +1,9 @@
import { LabelModel } from "../../models/LabelModel";
import * as _ from "lodash";
import { DeserializeEvent } from "@projectstorm/react-canvas";
export class DefaultLabelModel extends LabelModel {
label: string;
protected label: string;
constructor() {
super("default");
@@ -11,4 +13,15 @@ export class DefaultLabelModel extends LabelModel {
setLabel(label: string) {
this.label = label;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.label = event.data.label;
}
serialize() {
return _.merge(super.serialize(), {
label: this.label
});
}
}

View File

@@ -1,13 +1,8 @@
/**
* @author Dylan Vorster
*/
import { LinkModel, LinkModelListener } from "../../models/LinkModel";
import { BaseEvent } from "../../BaseEntity";
import * as _ from "lodash";
import { PointModel } from "../../models/PointModel";
import { DiagramEngine } from "../../DiagramEngine";
import { DefaultLabelModel } from "./DefaultLabelModel";
import { LabelModel } from "../../models/LabelModel";
import { BaseEvent, DeserializeEvent } from "@projectstorm/react-canvas";
export interface DefaultLinkModelListener extends LinkModelListener {
colorChanged?(event: BaseEvent<DefaultLinkModel> & { color: null | string }): void;
@@ -16,9 +11,9 @@ export interface DefaultLinkModelListener extends LinkModelListener {
}
export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
width: number;
color: string;
curvyness: number;
protected width: number;
protected color: string;
protected curvyness: number;
constructor(type: string = "default") {
super(type);
@@ -35,11 +30,11 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
});
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.color = ob.color;
this.width = ob.width;
this.curvyness = ob.curvyness;
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.color = event.data.color;
this.width = event.data.width;
this.curvyness = event.data.curvyness;
}
addLabel(label: LabelModel | string) {
@@ -53,7 +48,7 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
setWidth(width: number) {
this.width = width;
this.iterateListeners((listener: DefaultLinkModelListener, event: BaseEvent) => {
this.iterateListeners("width changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
if (listener.widthChanged) {
listener.widthChanged({ ...event, width: width });
}
@@ -62,10 +57,22 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
setColor(color: string) {
this.color = color;
this.iterateListeners((listener: DefaultLinkModelListener, event: BaseEvent) => {
this.iterateListeners("color changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
if (listener.colorChanged) {
listener.colorChanged({ ...event, color: color });
}
});
}
getWidth() {
return this.width;
}
getColor() {
return this.color;
}
getCurvyness() {
return this.curvyness;
}
}

View File

@@ -1,17 +1,12 @@
import { DefaultPortModel } from "./DefaultPortModel";
import * as _ from "lodash";
import { DefaultPortModel } from "./DefaultPortModel";
import { NodeModel } from "../../models/NodeModel";
import { Toolkit } from "../../Toolkit";
import { DiagramEngine } from "../../DiagramEngine";
import { DeserializeEvent } from "@projectstorm/react-canvas";
/**
* @author Dylan Vorster
*/
export class DefaultNodeModel extends NodeModel {
name: string;
color: string;
ports: { [s: string]: DefaultPortModel };
export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
protected name: string;
protected color: string;
constructor(name: string = "Untitled", color: string = "rgb(0,192,255)") {
super("default");
@@ -27,10 +22,10 @@ export class DefaultNodeModel extends NodeModel {
return this.addPort(new DefaultPortModel(false, Toolkit.UID(), label));
}
deSerialize(object, engine: DiagramEngine) {
super.deSerialize(object, engine);
this.name = object.name;
this.color = object.color;
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.name = event.data.name;
this.color = event.data.color;
}
serialize() {
@@ -41,13 +36,13 @@ export class DefaultNodeModel extends NodeModel {
}
getInPorts(): DefaultPortModel[] {
return _.filter(this.ports, portModel => {
return _.filter(this.getPorts(), portModel => {
return portModel.in;
});
}
getOutPorts(): DefaultPortModel[] {
return _.filter(this.ports, portModel => {
return _.filter(this.getPorts(), portModel => {
return !portModel.in;
});
}

View File

@@ -1,24 +1,24 @@
import * as _ from "lodash";
import { PortModel } from "../../models/PortModel";
import { DiagramEngine } from "../../DiagramEngine";
import { DefaultLinkModel } from "./DefaultLinkModel";
import { LinkModel } from "../../models/LinkModel";
import { DeserializeEvent } from "@projectstorm/react-canvas";
export class DefaultPortModel extends PortModel {
in: boolean;
label: string;
links: { [id: string]: DefaultLinkModel };
constructor(isInput: boolean, name: string, label: string = null, id?: string) {
super(name, "default", id);
constructor(isInput: boolean, name: string, label: string = null) {
super(name, "default");
this.in = isInput;
this.label = label || name;
}
deSerialize(object, engine: DiagramEngine) {
super.deSerialize(object, engine);
this.in = object.in;
this.label = object.label;
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.in = event.data.in;
this.label = event.data.label;
}
serialize() {

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { DefaultLabelModel } from "../models/DefaultLabelModel";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultLabelWidgetProps extends BaseWidgetProps {
model: DefaultLabelModel;

View File

@@ -7,7 +7,7 @@ import { DefaultLinkModel } from "../models/DefaultLinkModel";
import PathFinding from "../../routing/PathFinding";
import * as _ from "lodash";
import { LabelModel } from "../../models/LabelModel";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultLinkProps extends BaseWidgetProps {
color?: string;
@@ -70,7 +70,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
}
}
addPointToLink = (event: MouseEvent, index: number): void => {
addPointToLink(event: MouseEvent, index: number) {
if (
!event.shiftKey &&
!this.props.diagramEngine.isModelLocked(this.props.link) &&
@@ -82,14 +82,14 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
this.props.link.addPoint(point, index);
this.props.pointAdded(point, event);
}
};
}
generatePoint(pointIndex: number): JSX.Element {
let x = this.props.link.points[pointIndex].x;
let y = this.props.link.points[pointIndex].y;
let x = this.props.link.points[pointIndex].getPoint().x;
let y = this.props.link.points[pointIndex].getPoint().y;
return (
<g key={"point-" + this.props.link.points[pointIndex].id}>
<g key={"point-" + this.props.link.points[pointIndex].getID()}>
<circle
cx={x}
cy={y}
@@ -107,8 +107,8 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
onMouseEnter={() => {
this.setState({ selected: true });
}}
data-id={this.props.link.points[pointIndex].id}
data-linkid={this.props.link.id}
data-id={this.props.link.points[pointIndex].getID()}
data-linkid={this.props.link.getID()}
cx={x}
cy={y}
r={15}
@@ -123,15 +123,15 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
const canvas = this.props.diagramEngine.canvas as HTMLElement;
return (
<foreignObject
key={label.id}
key={label.getID()}
className={this.bem("__label")}
width={canvas.offsetWidth}
height={canvas.offsetHeight}
>
<div ref={ref => (this.refLabels[label.id] = ref)}>
<div ref={ref => (this.refLabels[label.getID()] = ref)}>
{this.props.diagramEngine
.getFactoryForLabel(label)
.generateReactWidget(this.props.diagramEngine, label)}
.getFactoryForElement(label)
.generateWidget(this.props.diagramEngine, label)}
</div>
</foreignObject>
);
@@ -141,7 +141,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
var props = this.props;
var Bottom = React.cloneElement(
(props.diagramEngine.getFactoryForLink(this.props.link) as DefaultLinkFactory).generateLinkSegment(
(props.diagramEngine.getFactoryForElement(this.props.link) as DefaultLinkFactory).generateLinkSegment(
this.props.link,
this,
this.state.selected || this.props.link.isSelected(),
@@ -267,7 +267,10 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
if (this.isSmartRoutingApplicable()) {
// first step: calculate a direct path between the points being linked
const directPathCoords = this.pathFinding.calculateDirectPath(_.first(points), _.last(points));
const directPathCoords = this.pathFinding.calculateDirectPath(
_.first(points).getPoint(),
_.last(points).getPoint()
);
const routingMatrix = diagramEngine.getRoutingMatrix();
// now we need to extract, from the routing matrix, the very first walkable points
@@ -305,7 +308,9 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
// See @link{#isSmartRoutingApplicable()}.
if (paths.length === 0) {
if (points.length === 2) {
var isHorizontal = Math.abs(points[0].x - points[1].x) > Math.abs(points[0].y - points[1].y);
var isHorizontal =
Math.abs(points[0].getPoint().x - points[1].getPoint().x) >
Math.abs(points[0].getPoint().x - points[1].getPoint().y);
var xOrY = isHorizontal ? "x" : "y";
//draw the smoothing
@@ -327,7 +332,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
paths.push(
this.generateLink(
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.curvyness),
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.getCurvyness()),
{
onMouseDown: event => {
this.addPointToLink(event, 1);
@@ -348,7 +353,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
this.generateLink(
Toolkit.generateLinePath(points[j], points[j + 1]),
{
"data-linkid": this.props.link.id,
"data-linkid": this.props.link.getID(),
"data-point": j,
onMouseDown: (event: MouseEvent) => {
this.addPointToLink(event, j + 1);

View File

@@ -3,7 +3,7 @@ import * as _ from "lodash";
import { DefaultNodeModel } from "../models/DefaultNodeModel";
import { DefaultPortLabel } from "./DefaultPortLabelWidget";
import { DiagramEngine } from "../../DiagramEngine";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultNodeProps extends BaseWidgetProps {
node: DefaultNodeModel;

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { DefaultPortModel } from "../models/DefaultPortModel";
import { PortWidget } from "../../widgets/PortWidget";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultPortLabelProps extends BaseWidgetProps {
model: DefaultPortModel;

View File

@@ -1,15 +0,0 @@
import { BaseModel } from "../models/BaseModel";
export abstract class AbstractFactory<T extends BaseModel> {
type: string;
constructor(name: string) {
this.type = name;
}
getType(): string {
return this.type;
}
abstract getNewInstance(initialConfig?: any): T;
}

View File

@@ -1,7 +0,0 @@
import { LabelModel } from "../models/LabelModel";
import { DiagramEngine } from "../DiagramEngine";
import { AbstractFactory } from "./AbstractFactory";
export abstract class AbstractLabelFactory<T extends LabelModel = LabelModel> extends AbstractFactory<T> {
abstract generateReactWidget(diagramEngine: DiagramEngine, link: T): JSX.Element;
}

View File

@@ -1,7 +0,0 @@
import { LinkModel } from "../models/LinkModel";
import { DiagramEngine } from "../DiagramEngine";
import { AbstractFactory } from "./AbstractFactory";
export abstract class AbstractLinkFactory<T extends LinkModel = LinkModel> extends AbstractFactory<T> {
abstract generateReactWidget(diagramEngine: DiagramEngine, link: T): JSX.Element;
}

View File

@@ -1,7 +0,0 @@
import { NodeModel } from "../models/NodeModel";
import { DiagramEngine } from "../DiagramEngine";
import { AbstractFactory } from "./AbstractFactory";
export abstract class AbstractNodeFactory<T extends NodeModel = NodeModel> extends AbstractFactory<T> {
abstract generateReactWidget(diagramEngine: DiagramEngine, node: T): JSX.Element;
}

View File

@@ -1,5 +0,0 @@
import { PortModel } from "../models/PortModel";
import { DiagramEngine } from "../DiagramEngine";
import { AbstractFactory } from "./AbstractFactory";
export abstract class AbstractPortFactory<T extends PortModel = PortModel> extends AbstractFactory<T> {}

View File

@@ -3,7 +3,6 @@
*/
export * from "./Toolkit";
export * from "./BaseEntity";
export * from "./DiagramEngine";
export * from "./defaults/models/DefaultNodeModel";
@@ -21,21 +20,8 @@ export * from "./defaults/widgets/DefaultLabelWidget";
export * from "./defaults/widgets/DefaultNodeWidget";
export * from "./defaults/widgets/DefaultPortLabelWidget";
export * from "./factories/AbstractFactory";
export * from "./factories/AbstractLabelFactory";
export * from "./factories/AbstractLinkFactory";
export * from "./factories/AbstractNodeFactory";
export * from "./factories/AbstractPortFactory";
export * from "./routing/PathFinding";
export * from "./actions/BaseAction";
export * from "./actions/MoveCanvasAction";
export * from "./actions/MoveItemsAction";
export * from "./actions/SelectingAction";
export * from "./models/SelectionModel";
export * from "./models/BaseModel";
export * from "./models/DiagramModel";
export * from "./models/LinkModel";
export * from "./models/NodeModel";
@@ -43,11 +29,5 @@ export * from "./models/PointModel";
export * from "./models/PortModel";
export * from "./models/LabelModel";
export * from "./widgets/DiagramWidget";
export * from "./widgets/LinkWidget";
export * from "./widgets/NodeWidget";
export * from "./widgets/PortWidget";
export * from "./widgets/BaseWidget";
export * from "./widgets/layers/LinkLayerWidget";
export * from "./widgets/layers/NodeLayerWidget";

View File

@@ -1,85 +0,0 @@
import { BaseEntity, BaseListener } from "../BaseEntity";
import * as _ from "lodash";
import { BaseEvent } from "../BaseEntity";
import { DiagramEngine } from "../DiagramEngine";
export interface BaseModelListener extends BaseListener {
selectionChanged?(event: BaseEvent<BaseModel> & { isSelected: boolean }): void;
entityRemoved?(event: BaseEvent<BaseModel>): void;
}
/**
* @author Dylan Vorster
*/
export class BaseModel<
X extends BaseEntity = BaseEntity,
T extends BaseModelListener = BaseModelListener
> extends BaseEntity<T> {
type: string;
selected: boolean;
parent: X;
constructor(type?: string, id?: string) {
super(id);
this.type = type;
this.selected = false;
}
public getParent(): X {
return this.parent;
}
public setParent(parent: X) {
this.parent = parent;
}
public getSelectedEntities(): BaseModel<any, T>[] {
if (this.isSelected()) {
return [this];
}
return [];
}
public deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.type = ob.type;
this.selected = ob.selected;
}
public serialize() {
return _.merge(super.serialize(), {
type: this.type,
selected: this.selected
});
}
public getType(): string {
return this.type;
}
public getID(): string {
return this.id;
}
public isSelected(): boolean {
return this.selected;
}
public setSelected(selected: boolean = true) {
this.selected = selected;
this.iterateListeners((listener, event) => {
if (listener.selectionChanged) {
listener.selectionChanged({ ...event, isSelected: selected });
}
});
}
public remove() {
this.iterateListeners((listener, event) => {
if (listener.entityRemoved) {
listener.entityRemoved(event);
}
});
}
}

View File

@@ -1,240 +1,48 @@
import { BaseListener, BaseEntity, BaseEvent, BaseEntityType } from "../BaseEntity";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "./LinkModel";
import { NodeModel } from "./NodeModel";
import { PortModel } from "./PortModel";
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PointModel } from "./PointModel";
/**
* @author Dylan Vorster
*
*/
export interface DiagramListener extends BaseListener {
import { BaseModel, BaseEvent, CanvasModel, CanvasModelListener, CanvasLayerModel } from "@projectstorm/react-canvas";
export interface DiagramListener extends CanvasModelListener {
nodesUpdated?(event: BaseEvent & { node: NodeModel; isCreated: boolean }): void;
linksUpdated?(event: BaseEvent & { link: LinkModel; isCreated: boolean }): void;
offsetUpdated?(event: BaseEvent<DiagramModel> & { offsetX: number; offsetY: number }): void;
zoomUpdated?(event: BaseEvent<DiagramModel> & { zoom: number }): void;
gridUpdated?(event: BaseEvent<DiagramModel> & { size: number }): void;
}
/**
*
*/
export class DiagramModel extends BaseEntity<DiagramListener> {
//models
links: { [s: string]: LinkModel };
nodes: { [s: string]: NodeModel };
//control variables
offsetX: number;
offsetY: number;
zoom: number;
rendered: boolean;
gridSize: number;
export class DiagramModel extends CanvasModel<DiagramListener> {
linksLayer: CanvasLayerModel<LinkModel>;
nodesLayer: CanvasLayerModel<NodeModel>;
constructor() {
super();
this.links = {};
this.nodes = {};
this.linksLayer = new CanvasLayerModel();
this.nodesLayer = new CanvasLayerModel();
this.offsetX = 0;
this.offsetY = 0;
this.zoom = 100;
this.rendered = false;
this.gridSize = 0;
}
this.linksLayer.setSVG(true);
this.linksLayer.setTransformable(true);
setGridSize(size: number = 0) {
this.gridSize = size;
this.iterateListeners((listener, event) => {
if (listener.gridUpdated) {
listener.gridUpdated({ ...event, size: size });
}
});
}
this.nodesLayer.setSVG(false);
this.nodesLayer.setTransformable(true);
getGridPosition(pos) {
if (this.gridSize === 0) {
return pos;
}
return this.gridSize * Math.floor((pos + this.gridSize / 2) / this.gridSize);
}
deSerializeDiagram(object: any, diagramEngine: DiagramEngine) {
this.deSerialize(object, diagramEngine);
this.offsetX = object.offsetX;
this.offsetY = object.offsetY;
this.zoom = object.zoom;
this.gridSize = object.gridSize;
// deserialize nodes
_.forEach(object.nodes, (node: any) => {
let nodeOb = diagramEngine.getNodeFactory(node.type).getNewInstance(node);
nodeOb.setParent(this);
nodeOb.deSerialize(node, diagramEngine);
this.addNode(nodeOb);
});
// deserialze links
_.forEach(object.links, (link: any) => {
let linkOb = diagramEngine.getLinkFactory(link.type).getNewInstance();
linkOb.setParent(this);
linkOb.deSerialize(link, diagramEngine);
this.addLink(linkOb);
});
}
serializeDiagram() {
return _.merge(this.serialize(), {
offsetX: this.offsetX,
offsetY: this.offsetY,
zoom: this.zoom,
gridSize: this.gridSize,
links: _.map(this.links, link => {
return link.serialize();
}),
nodes: _.map(this.nodes, node => {
return node.serialize();
})
});
}
clearSelection(ignore: BaseModel<BaseEntity, BaseModelListener> | null = null) {
_.forEach(this.getSelectedItems(), element => {
if (ignore && ignore.getID() === element.getID()) {
return;
}
element.setSelected(false); //TODO dont fire the listener
});
}
getSelectedItems(...filters: BaseEntityType[]): BaseModel<BaseEntity, BaseModelListener>[] {
if (!Array.isArray(filters)) {
filters = [filters];
}
var items = [];
// run through nodes
items = items.concat(
_.flatMap(this.nodes, node => {
return node.getSelectedEntities();
})
);
// find all the links
items = items.concat(
_.flatMap(this.links, link => {
return link.getSelectedEntities();
})
);
//find all points
items = items.concat(
_.flatMap(this.links, link => {
return _.flatMap(link.points, point => {
return point.getSelectedEntities();
});
})
);
items = _.uniq(items);
if (filters.length > 0) {
items = _.filter(_.uniq(items), (item: BaseModel<any>) => {
if (_.includes(filters, "node") && item instanceof NodeModel) {
return true;
}
if (_.includes(filters, "link") && item instanceof LinkModel) {
return true;
}
if (_.includes(filters, "port") && item instanceof PortModel) {
return true;
}
if (_.includes(filters, "point") && item instanceof PointModel) {
return true;
}
return false;
});
}
return items;
}
setZoomLevel(zoom: number) {
this.zoom = zoom;
this.iterateListeners((listener, event) => {
if (listener.zoomUpdated) {
listener.zoomUpdated({ ...event, zoom: zoom });
}
});
}
setOffset(offsetX: number, offsetY: number) {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.iterateListeners((listener, event) => {
if (listener.offsetUpdated) {
listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: offsetY });
}
});
}
setOffsetX(offsetX: number) {
this.offsetX = offsetX;
this.iterateListeners((listener, event) => {
if (listener.offsetUpdated) {
listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: this.offsetY });
}
});
}
setOffsetY(offsetY: number) {
this.offsetY = offsetY;
this.iterateListeners((listener, event) => {
if (listener.offsetUpdated) {
listener.offsetUpdated({ ...event, offsetX: this.offsetX, offsetY: this.offsetY });
}
});
}
getOffsetY() {
return this.offsetY;
}
getOffsetX() {
return this.offsetX;
}
getZoomLevel() {
return this.zoom;
this.addLayer(this.linksLayer);
this.addLayer(this.nodesLayer);
}
getNode(node: string | NodeModel): NodeModel | null {
if (node instanceof NodeModel) {
return node;
}
if (!this.nodes[node]) {
return null;
}
return this.nodes[node];
return this.nodesLayer.getEntity[node] || null;
}
getLink(link: string | LinkModel): LinkModel | null {
if (link instanceof LinkModel) {
return link;
}
if (!this.links[link]) {
return null;
}
return this.links[link];
return this.linksLayer.getEntity(link) || null;
}
addAll(...models: BaseModel[]): BaseModel[] {
@@ -249,13 +57,8 @@ export class DiagramModel extends BaseEntity<DiagramListener> {
}
addLink(link: LinkModel): LinkModel {
link.addListener({
entityRemoved: () => {
this.removeLink(link);
}
});
this.links[link.getID()] = link;
this.iterateListeners((listener, event) => {
this.linksLayer.addEntity(link);
this.iterateListeners("link added", (listener, event) => {
if (listener.linksUpdated) {
listener.linksUpdated({ ...event, link: link, isCreated: true });
}
@@ -264,13 +67,8 @@ export class DiagramModel extends BaseEntity<DiagramListener> {
}
addNode(node: NodeModel): NodeModel {
node.addListener({
entityRemoved: () => {
this.removeNode(node);
}
});
this.nodes[node.getID()] = node;
this.iterateListeners((listener, event) => {
this.nodesLayer.addEntity(node);
this.iterateListeners("node added", (listener, event) => {
if (listener.nodesUpdated) {
listener.nodesUpdated({ ...event, node: node, isCreated: true });
}
@@ -279,9 +77,8 @@ export class DiagramModel extends BaseEntity<DiagramListener> {
}
removeLink(link: LinkModel | string) {
link = this.getLink(link);
delete this.links[link.getID()];
this.iterateListeners((listener, event) => {
this.linksLayer.removeEntity(link);
this.iterateListeners("link removed", (listener, event) => {
if (listener.linksUpdated) {
listener.linksUpdated({ ...event, link: link as LinkModel, isCreated: false });
}
@@ -289,9 +86,8 @@ export class DiagramModel extends BaseEntity<DiagramListener> {
}
removeNode(node: NodeModel | string) {
node = this.getNode(node);
delete this.nodes[node.getID()];
this.iterateListeners((listener, event) => {
this.nodesLayer.removeEntity(node);
this.iterateListeners("node removed", (listener, event) => {
if (listener.nodesUpdated) {
listener.nodesUpdated({ ...event, node: node as NodeModel, isCreated: false });
}
@@ -299,10 +95,10 @@ export class DiagramModel extends BaseEntity<DiagramListener> {
}
getLinks(): { [s: string]: LinkModel } {
return this.links;
return this.linksLayer.getEntities();
}
getNodes(): { [s: string]: NodeModel } {
return this.nodes;
return this.nodesLayer.getEntities();
}
}

View File

@@ -1,13 +1,27 @@
import { BaseModel } from "./BaseModel";
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
import { BaseModel, DeserializeEvent } from "@projectstorm/react-canvas";
export class LabelModel extends BaseModel<LinkModel> {
offsetX: number;
offsetY: number;
constructor(type?: string, id?: string) {
super(type, id);
constructor(type?: string) {
super(type);
this.offsetX = 0;
this.offsetY = 0;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.offsetX = event.data.offsetX;
this.offsetY = event.data.offsetY;
}
serialize() {
return _.merge(super.serialize(), {
offsetX: this.offsetX,
offsetY: this.offsetY
});
}
}

View File

@@ -1,97 +1,71 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PortModel } from "./PortModel";
import { PointModel } from "./PointModel";
import * as _ from "lodash";
import { BaseEvent } from "../BaseEntity";
import { LabelModel } from "./LabelModel";
import { DiagramEngine } from "../DiagramEngine";
import { DiagramModel } from "./DiagramModel";
import {
BaseEvent,
GraphModel,
GraphModelOrdered,
CanvasElementModel,
CanvasElementModelListener,
Rectangle,
DeserializeEvent
} from "@projectstorm/react-canvas";
export interface LinkModelListener extends BaseModelListener {
sourcePortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
export interface LinkModelListener<T extends LinkModel = any> extends CanvasElementModelListener<T> {
sourcePortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
}
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends BaseModel<DiagramModel, T> {
sourcePort: PortModel | null;
targetPort: PortModel | null;
labels: LabelModel[];
points: PointModel[];
extras: {};
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends CanvasElementModel<T> {
protected sourcePort: PortModel | null;
protected targetPort: PortModel | null;
protected labels: GraphModel<LabelModel, LinkModel>;
protected points: GraphModelOrdered<PointModel, LinkModel>;
constructor(linkType: string = "default", id?: string) {
super(linkType, id);
this.points = [new PointModel(this, { x: 0, y: 0 }), new PointModel(this, { x: 0, y: 0 })];
this.extras = {};
constructor(linkType: string = "default") {
super(linkType);
this.points = new GraphModelOrdered();
this.labels = new GraphModel();
this.points.setParentDelegate(this);
this.labels.setParentDelegate(this);
this.sourcePort = null;
this.targetPort = null;
this.labels = [];
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.extras = ob.extras;
this.points = _.map(ob.points || [], (point: { x; y }) => {
var p = new PointModel(this, { x: point.x, y: point.y });
p.deSerialize(point, engine);
return p;
});
setDimensions(dimensions: Rectangle) {
throw new Error("Method not implemented.");
}
//deserialize labels
_.forEach(ob.labels || [], (label: any) => {
let labelOb = engine.getLabelFactory(label.type).getNewInstance();
labelOb.deSerialize(label, engine);
this.addLabel(labelOb);
});
getDimensions(): Rectangle {
throw new Error("Method not implemented.");
}
if (ob.target) {
this.setTargetPort(
this.getParent()
.getNode(ob.target)
.getPortFromID(ob.targetPort)
);
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.points.deSerialize(event.subset("points"));
this.labels.deSerialize(event.subset("labels"));
if (event.data.target) {
this.setTargetPort(event.cache[event.data.targetPort] as PortModel);
}
if (ob.source) {
this.setSourcePort(
this.getParent()
.getNode(ob.source)
.getPortFromID(ob.sourcePort)
);
if (event.data.source) {
this.setSourcePort(event.cache[event.data.sourcePort] as PortModel);
}
}
serialize() {
return _.merge(super.serialize(), {
source: this.sourcePort ? this.sourcePort.getParent().id : null,
sourcePort: this.sourcePort ? this.sourcePort.id : null,
target: this.targetPort ? this.targetPort.getParent().id : null,
targetPort: this.targetPort ? this.targetPort.id : null,
points: _.map(this.points, point => {
return point.serialize();
}),
extras: this.extras,
labels: _.map(this.labels, label => {
return label.serialize();
})
source: this.sourcePort ? this.sourcePort.getParent().getID() : null,
sourcePort: this.sourcePort ? this.sourcePort.getID() : null,
target: this.targetPort ? this.targetPort.getParent().getID() : null,
targetPort: this.targetPort ? this.targetPort.getID() : null,
points: this.points.serialize(),
labels: this.labels.serialize()
});
}
doClone(lookupTable = {}, clone) {
clone.setPoints(
_.map(this.getPoints(), (point: PointModel) => {
return point.clone(lookupTable);
})
);
if (this.sourcePort) {
clone.setSourcePort(this.sourcePort.clone(lookupTable));
}
if (this.targetPort) {
clone.setTargetPort(this.targetPort.clone(lookupTable));
}
}
remove() {
if (this.sourcePort) {
this.sourcePort.removeLink(this);
@@ -99,25 +73,19 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
if (this.targetPort) {
this.targetPort.removeLink(this);
}
super.remove();
}
isLastPoint(point: PointModel) {
var index = this.getPointIndex(point);
return index === this.points.length - 1;
return index === this.points.count() - 1;
}
getPointIndex(point: PointModel) {
return this.points.indexOf(point);
return _.values(this.points.getEntities()).indexOf(point);
}
getPointModel(id: string): PointModel | null {
for (var i = 0; i < this.points.length; i++) {
if (this.points[i].id === id) {
return this.points[i];
}
}
return null;
return this.points.getEntities()[id];
}
getPortForPoint(point: PointModel): PortModel {
@@ -141,23 +109,22 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
}
getFirstPoint(): PointModel {
return this.points[0];
return _.values(this.points.getEntities())[0];
}
getLastPoint(): PointModel {
return this.points[this.points.length - 1];
return _.values(this.points.getEntities())[this.points.count() - 1];
}
setSourcePort(port: PortModel) {
if (port !== null) {
port.addLink(this);
} else if (this.sourcePort !== null) {
}
if (this.sourcePort !== null) {
this.sourcePort.removeLink(this);
} else {
return;
}
this.sourcePort = port;
this.iterateListeners((listener: LinkModelListener, event) => {
this.iterateListeners("source port changed", (listener: T, event) => {
if (listener.sourcePortChanged) {
listener.sourcePortChanged({ ...event, port: port });
}
@@ -175,13 +142,12 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
setTargetPort(port: PortModel) {
if (port !== null) {
port.addLink(this);
} else if (this.targetPort !== null) {
}
if (this.targetPort !== null) {
this.targetPort.removeLink(this);
} else {
return;
}
this.targetPort = port;
this.iterateListeners((listener: LinkModelListener, event) => {
this.iterateListeners("target port chnaged", (listener: T, event) => {
if (listener.targetPortChanged) {
listener.targetPortChanged({ ...event, port: port });
}
@@ -193,46 +159,34 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
}
addLabel(label: LabelModel) {
label.setParent(this);
this.labels.push(label);
this.labels.addEntity(label);
}
getPoints(): PointModel[] {
return this.points;
return this.points.getArray();
}
setPoints(points: PointModel[]) {
_.forEach(points, point => {
point.setParent(this);
point.setLink(this);
});
this.points = points;
this.points.addEntities(points);
}
removePoint(pointModel: PointModel) {
this.points.splice(this.getPointIndex(pointModel), 1);
}
removePointsBefore(pointModel: PointModel) {
this.points.splice(0, this.getPointIndex(pointModel));
}
removePointsAfter(pointModel: PointModel) {
this.points.splice(this.getPointIndex(pointModel) + 1);
}
removeMiddlePoints() {
if (this.points.length > 2) {
this.points.splice(0, this.points.length - 2);
}
this.points.removeEntity(pointModel);
}
addPoint<P extends PointModel>(pointModel: P, index = 1): P {
pointModel.setParent(this);
this.points.splice(index, 0, pointModel);
pointModel.setLink(this);
this.points.addEntity(pointModel, index);
return pointModel;
}
generatePoint(x: number = 0, y: number = 0): PointModel {
return new PointModel(this, { x: x, y: y });
generatePoint(x: number, y: number): PointModel {
let point = new PointModel(this);
point.getPoint().x = x;
point.getPoint().y = y;
return point;
}
}

View File

@@ -1,118 +1,67 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PortModel } from "./PortModel";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
import { DiagramModel } from "./DiagramModel";
import { Rectangle, CanvasElementModel, GraphModel, DeserializeEvent } from "@projectstorm/react-canvas";
export class NodeModel extends BaseModel<DiagramModel, BaseModelListener> {
x: number;
y: number;
extras: any;
ports: { [s: string]: PortModel };
export class NodeModel<T extends PortModel = PortModel> extends CanvasElementModel {
protected dimensions: Rectangle;
protected ports: GraphModel<T, null>;
// calculated post rendering so routing can be done correctly
width: number;
height: number;
constructor(nodeType: string = "default", id?: string) {
super(nodeType, id);
this.x = 0;
this.y = 0;
this.extras = {};
this.ports = {};
constructor(nodeType: string = "default") {
super(nodeType);
this.dimensions = new Rectangle(0, 0, 0, 0);
this.ports = new GraphModel("ports");
}
setPosition(x, y) {
//store position
let oldX = this.x;
let oldY = this.y;
_.forEach(this.ports, port => {
_.forEach(port.getLinks(), link => {
let point = link.getPointForPort(port);
point.x = point.x + x - oldX;
point.y = point.y + y - oldY;
});
});
this.x = x;
this.y = y;
setDimensions(dimensions: Rectangle) {
this.dimensions = dimensions;
}
getSelectedEntities() {
let entities = super.getSelectedEntities();
// add the points of each link that are selected here
if (this.isSelected()) {
_.forEach(this.ports, port => {
entities = entities.concat(
_.map(port.getLinks(), link => {
return link.getPointForPort(port);
})
);
});
}
return entities;
getDimensions(): Rectangle {
return this.dimensions;
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.x = ob.x;
this.y = ob.y;
this.extras = ob.extras;
setPosition(x:number, y:number){
this.dimensions.updateDimensions(x,y, this.dimensions.getWidth(), this.dimensions.getHeight());
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.dimensions.deserialize(event.data.dimensions);
//deserialize ports
_.forEach(ob.ports, (port: any) => {
let portOb = engine.getPortFactory(port.type).getNewInstance();
portOb.deSerialize(port, engine);
let ports = event.subset("ports");
_.forEach(ports.data, (port: any, index) => {
let portOb = event.engine.getFactory(port.type).generateModel() as T;
portOb.deSerialize(ports.subset(index));
this.addPort(portOb);
});
}
serialize() {
return _.merge(super.serialize(), {
x: this.x,
y: this.y,
extras: this.extras,
ports: _.map(this.ports, port => {
return port.serialize();
})
dimensions: this.dimensions.serialize(),
ports: this.ports.serialize()
});
}
doClone(lookupTable = {}, clone) {
// also clone the ports
clone.ports = {};
_.forEach(this.ports, port => {
clone.addPort(port.clone(lookupTable));
});
}
remove() {
super.remove();
_.forEach(this.ports, port => {
_.forEach(port.getLinks(), link => {
link.remove();
});
});
}
getPortFromID(id): PortModel | null {
for (var i in this.ports) {
if (this.ports[i].id === id) {
getPortFromID(id): T | null {
for (let i in this.ports) {
if (this.ports[i].getID() === id) {
return this.ports[i];
}
}
return null;
}
getPort(name: string): PortModel | null {
return this.ports[name];
getPort(name: string): T | null {
return this.ports.getEntities()[name];
}
getPorts(): { [s: string]: PortModel } {
return this.ports;
getPorts(): { [s: string]: T } {
return this.ports.getEntities();
}
removePort(port: PortModel) {
removePort(port: T) {
//clear the parent node reference
if (this.ports[port.name]) {
this.ports[port.name].setParent(null);
@@ -120,14 +69,8 @@ export class NodeModel extends BaseModel<DiagramModel, BaseModelListener> {
}
}
addPort<T extends PortModel>(port: T): T {
port.setParent(this);
this.ports[port.name] = port;
addPort(port: T): T {
this.ports.addEntity(port);
return port;
}
updateDimensions({ width, height }: { width: number; height: number }) {
this.width = width;
this.height = height;
}
}

View File

@@ -1,69 +1,62 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
import {
Point,
CanvasElementModel,
CanvasElementModelListener,
Rectangle,
DeserializeEvent
} from "@projectstorm/react-canvas";
export class PointModel extends BaseModel<LinkModel, BaseModelListener> {
x: number;
y: number;
export class PointModel extends CanvasElementModel<CanvasElementModelListener> {
protected point: Point;
protected link: LinkModel;
constructor(link: LinkModel, points: { x: number; y: number }) {
super();
this.x = points.x;
this.y = points.y;
this.parent = link;
constructor(link: LinkModel) {
super("point");
this.link = link;
}
getSelectedEntities() {
if (super.isSelected() && !this.isConnectedToPort()) {
return [this];
}
return [];
setDimensions(dimensions: Rectangle) {
this.point = dimensions.getTopLeft();
}
getDimensions(): Rectangle {
return new Rectangle(this.point, 10, 10);
}
isConnectedToPort(): boolean {
return this.parent.getPortForPoint(this) !== null;
return this.link.getPortForPoint(this) !== null;
}
setLink(link: LinkModel) {
this.link = link;
}
getLink(): LinkModel {
return this.getParent();
return this.link;
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.x = ob.x;
this.y = ob.y;
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.point = new Point(event.data["x"], event.data["y"]);
}
serialize() {
return _.merge(super.serialize(), {
x: this.x,
y: this.y
x: this.point.x,
y: this.point.y
});
}
remove() {
//clear references
if (this.parent) {
this.parent.removePoint(this);
if (this.link) {
this.link.removePoint(this);
}
super.remove();
}
updateLocation(points: { x: number; y: number }) {
this.x = points.x;
this.y = points.y;
}
getX(): number {
return this.x;
}
getY(): number {
return this.y;
}
isLocked() {
return super.isLocked() || this.getParent().isLocked();
getPoint(): Point {
return this.point;
}
}

View File

@@ -1,49 +1,37 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { BaseModel, BaseListener, DeserializeEvent } from "@projectstorm/react-canvas";
import { NodeModel } from "./NodeModel";
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
export class PortModel extends BaseModel<NodeModel, BaseModelListener> {
export class PortModel extends BaseModel<NodeModel, BaseListener> {
name: string;
links: { [id: string]: LinkModel };
maximumLinks: number;
// calculated post rendering so routing can be done correctly
x: number;
y: number;
width: number;
height: number;
constructor(name: string, type?: string, id?: string, maximumLinks?: number) {
super(type, id);
constructor(name: string, type?: string, maximumLinks?: number) {
super(type);
this.name = name;
this.links = {};
this.maximumLinks = maximumLinks;
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.name = ob.name;
this.maximumLinks = ob.maximumLinks;
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.name = event.data.name;
this.maximumLinks = event.data.maximumLinks;
}
serialize() {
return _.merge(super.serialize(), {
name: this.name,
parentNode: this.parent.id,
parentNode: this.parent.getID(),
links: _.map(this.links, link => {
return link.id;
return link.getID();
}),
maximumLinks: this.maximumLinks
});
}
doClone(lookupTable = {}, clone) {
clone.links = {};
clone.parentNode = this.getParent().clone(lookupTable);
}
getNode(): NodeModel {
return this.getParent();
}
@@ -84,18 +72,7 @@ export class PortModel extends BaseModel<NodeModel, BaseModelListener> {
return null;
}
updateCoords({ x, y, width, height }: { x: number; y: number; width: number; height: number }) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
canLinkToPort(port: PortModel): boolean {
return true;
}
isLocked() {
return super.isLocked() || this.getParent().isLocked();
}
}

View File

@@ -1,8 +0,0 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { BaseEntity } from "../BaseEntity";
export interface SelectionModel {
model: BaseModel<BaseEntity, BaseModelListener>;
initialX: number;
initialY: number;
}

View File

@@ -1,133 +0,0 @@
import * as PF from "pathfinding";
import { DiagramEngine } from "../main";
/*
it can be very expensive to calculate routes when every single pixel on the canvas
is individually represented. Using the factor below, we combine values in order
to achieve the best trade-off between accuracy and performance.
*/
export const ROUTING_SCALING_FACTOR = 5;
const pathFinderInstance = new PF.JumpPointFinder({
heuristic: PF.Heuristic.manhattan,
diagonalMovement: PF.DiagonalMovement.Never
});
export default class PathFinding {
instance: any;
diagramEngine: DiagramEngine;
constructor(diagramEngine: DiagramEngine) {
this.instance = pathFinderInstance;
this.diagramEngine = diagramEngine;
}
/**
* Taking as argument a fully unblocked walking matrix, this method
* finds a direct path from point A to B.
*/
calculateDirectPath(
from: {
x: number;
y: number;
},
to: {
x: number;
y: number;
}
): number[][] {
const matrix = this.diagramEngine.getCanvasMatrix();
const grid = new PF.Grid(matrix);
return pathFinderInstance.findPath(
this.diagramEngine.translateRoutingX(Math.floor(from.x / ROUTING_SCALING_FACTOR)),
this.diagramEngine.translateRoutingY(Math.floor(from.y / ROUTING_SCALING_FACTOR)),
this.diagramEngine.translateRoutingX(Math.floor(to.x / ROUTING_SCALING_FACTOR)),
this.diagramEngine.translateRoutingY(Math.floor(to.y / ROUTING_SCALING_FACTOR)),
grid
);
}
/**
* Using @link{#calculateDirectPath}'s result as input, we here
* determine the first walkable point found in the matrix that includes
* blocked paths.
*/
calculateLinkStartEndCoords(
matrix: number[][],
path: number[][]
): {
start: {
x: number;
y: number;
};
end: {
x: number;
y: number;
};
pathToStart: number[][];
pathToEnd: number[][];
} {
const startIndex = path.findIndex(point => matrix[point[1]][point[0]] === 0);
const endIndex =
path.length -
1 -
path
.slice()
.reverse()
.findIndex(point => matrix[point[1]][point[0]] === 0);
// are we trying to create a path exclusively through blocked areas?
// if so, let's fallback to the linear routing
if (startIndex === -1 || endIndex === -1) {
return undefined;
}
const pathToStart = path.slice(0, startIndex);
const pathToEnd = path.slice(endIndex);
return {
start: {
x: path[startIndex][0],
y: path[startIndex][1]
},
end: {
x: path[endIndex][0],
y: path[endIndex][1]
},
pathToStart,
pathToEnd
};
}
/**
* Puts everything together: merges the paths from/to the centre of the ports,
* with the path calculated around other elements.
*/
calculateDynamicPath(
routingMatrix: number[][],
start: {
x: number;
y: number;
},
end: {
x: number;
y: number;
},
pathToStart: number[][],
pathToEnd: number[][]
) {
// generate the path based on the matrix with obstacles
const grid = new PF.Grid(routingMatrix);
const dynamicPath = pathFinderInstance.findPath(start.x, start.y, end.x, end.y, grid);
// aggregate everything to have the calculated path ready for rendering
const pathCoords = pathToStart
.concat(dynamicPath, pathToEnd)
.map(coords => [
this.diagramEngine.translateRoutingX(coords[0], true),
this.diagramEngine.translateRoutingY(coords[1], true)
]);
return PF.Util.compressPath(pathCoords);
}
}

View File

@@ -1,11 +0,0 @@
.srd-link-layer{
position: absolute;
height: 100%;
width: 100%;
transform-origin: 0 0;
overflow: visible !important;
top:0;
bottom: 0;
left: 0;
right: 0;
}

View File

@@ -1,11 +0,0 @@
.srd-node-layer{
top:0;
left:0;
right:0;
bottom:0;
position: absolute;
pointer-events: none;
transform-origin: 0 0;
width: 100%;
height: 100%;
}

View File

@@ -1,6 +1,4 @@
@import "DiagramWidget";
@import "LinkLayerWidget";
@import "NodeLayerWidget";
@import "NodeWidget";
@import "PortWidget";
@@ -8,4 +6,4 @@
@import "defaults/DefaultNodeWidget";
@import "defaults/DefaultPortWidget";
@import "defaults/DefaultLabelWidget";
@import "defaults/DefaultLinkWidget";
@import "defaults/DefaultLinkWidget";

View File

@@ -1,44 +0,0 @@
import * as React from "react";
import * as _ from "lodash";
export interface BaseWidgetProps {
/**
* Override the base class name
*/
baseClass?: string;
/**
* append additional classes
*/
className?: string;
/**
* Additional props to add
*/
extraProps?: any;
}
export class BaseWidget<P extends BaseWidgetProps = BaseWidgetProps, S = any> extends React.Component<P, S> {
className: string;
constructor(name: string, props: P) {
super(props);
this.className = name;
}
bem(selector: string): string {
return (this.props.baseClass || this.className) + selector + " ";
}
getClassName(): string {
return (
(this.props.baseClass || this.className) + " " + (this.props.className ? this.props.className + " " : "")
);
}
getProps(): any {
return {
...((this.props.extraProps as any) || {}),
className: this.getClassName()
};
}
}

View File

@@ -1,559 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import * as _ from "lodash";
import { LinkLayerWidget } from "./layers/LinkLayerWidget";
import { NodeLayerWidget } from "./layers/NodeLayerWidget";
import { Toolkit } from "../Toolkit";
import { BaseAction } from "../actions/BaseAction";
import { MoveCanvasAction } from "../actions/MoveCanvasAction";
import { MoveItemsAction } from "../actions/MoveItemsAction";
import { SelectingAction } from "../actions/SelectingAction";
import { NodeModel } from "../models/NodeModel";
import { PointModel } from "../models/PointModel";
import { PortModel } from "../models/PortModel";
import { LinkModel } from "../models/LinkModel";
import { SelectionModel } from "../models/SelectionModel";
import { BaseModel, BaseModelListener } from "../models/BaseModel";
import { BaseEntity } from "../BaseEntity";
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
export interface DiagramProps extends BaseWidgetProps {
diagramEngine: DiagramEngine;
allowLooseLinks?: boolean;
allowCanvasTranslation?: boolean;
allowCanvasZoom?: boolean;
inverseZoom?: boolean;
maxNumberPointsPerLink?: number;
smartRouting?: boolean;
actionStartedFiring?: (action: BaseAction) => boolean;
actionStillFiring?: (action: BaseAction) => void;
actionStoppedFiring?: (action: BaseAction) => void;
deleteKeys?: number[];
}
export interface DiagramState {
action: BaseAction | null;
wasMoved: boolean;
renderedNodes: boolean;
windowListener: any;
diagramEngineListener: any;
document: any;
}
/**
* @author Dylan Vorster
*/
export class DiagramWidget extends BaseWidget<DiagramProps, DiagramState> {
public static defaultProps: DiagramProps = {
diagramEngine: null,
allowLooseLinks: true,
allowCanvasTranslation: true,
allowCanvasZoom: true,
inverseZoom: false,
maxNumberPointsPerLink: Infinity, // backwards compatible default
smartRouting: false,
deleteKeys: [46, 8]
};
onKeyUpPointer: (this: Window, ev: KeyboardEvent) => void = null;
constructor(props: DiagramProps) {
super("srd-diagram", props);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.state = {
action: null,
wasMoved: false,
renderedNodes: false,
windowListener: null,
diagramEngineListener: null,
document: null
};
}
componentWillUnmount() {
this.props.diagramEngine.removeListener(this.state.diagramEngineListener);
this.props.diagramEngine.setCanvas(null);
window.removeEventListener("keyup", this.onKeyUpPointer);
window.removeEventListener("mouseUp", this.onMouseUp);
window.removeEventListener("mouseMove", this.onMouseMove);
}
componentWillReceiveProps(nextProps: DiagramProps) {
if (this.props.diagramEngine !== nextProps.diagramEngine) {
this.props.diagramEngine.removeListener(this.state.diagramEngineListener);
const diagramEngineListener = nextProps.diagramEngine.addListener({
repaintCanvas: () => this.forceUpdate()
});
this.setState({ diagramEngineListener });
}
}
componentWillUpdate(nextProps: DiagramProps) {
if (this.props.diagramEngine.diagramModel.id !== nextProps.diagramEngine.diagramModel.id) {
this.setState({ renderedNodes: false });
nextProps.diagramEngine.diagramModel.rendered = true;
}
if (!nextProps.diagramEngine.diagramModel.rendered) {
this.setState({ renderedNodes: false });
nextProps.diagramEngine.diagramModel.rendered = true;
}
}
componentDidUpdate() {
if (!this.state.renderedNodes) {
this.setState({
renderedNodes: true
});
}
}
componentDidMount() {
this.onKeyUpPointer = this.onKeyUp.bind(this);
//add a keyboard listener
this.setState({
document: document,
renderedNodes: true,
diagramEngineListener: this.props.diagramEngine.addListener({
repaintCanvas: () => {
this.forceUpdate();
}
})
});
window.addEventListener("keyup", this.onKeyUpPointer, false);
// dont focus the window when in test mode - jsdom fails
if (process.env.NODE_ENV !== "test") {
window.focus();
}
}
/**
* Gets a model and element under the mouse cursor
*/
getMouseElement(event): { model: BaseModel<BaseEntity, BaseModelListener>; element: Element } {
var target = event.target as Element;
var diagramModel = this.props.diagramEngine.diagramModel;
//is it a port
var element = Toolkit.closest(target, ".port[data-name]");
if (element) {
var nodeElement = Toolkit.closest(target, ".node[data-nodeid]") as HTMLElement;
return {
model: diagramModel
.getNode(nodeElement.getAttribute("data-nodeid"))
.getPort(element.getAttribute("data-name")),
element: element
};
}
//look for a point
element = Toolkit.closest(target, ".point[data-id]");
if (element) {
return {
model: diagramModel
.getLink(element.getAttribute("data-linkid"))
.getPointModel(element.getAttribute("data-id")),
element: element
};
}
//look for a link
element = Toolkit.closest(target, "[data-linkid]");
if (element) {
return {
model: diagramModel.getLink(element.getAttribute("data-linkid")),
element: element
};
}
//look for a node
element = Toolkit.closest(target, ".node[data-nodeid]");
if (element) {
return {
model: diagramModel.getNode(element.getAttribute("data-nodeid")),
element: element
};
}
return null;
}
fireAction() {
if (this.state.action && this.props.actionStillFiring) {
this.props.actionStillFiring(this.state.action);
}
}
stopFiringAction(shouldSkipEvent?: boolean) {
if (this.props.actionStoppedFiring && !shouldSkipEvent) {
this.props.actionStoppedFiring(this.state.action);
}
this.setState({ action: null });
}
startFiringAction(action: BaseAction) {
var setState = true;
if (this.props.actionStartedFiring) {
setState = this.props.actionStartedFiring(action);
}
if (setState) {
this.setState({ action: action });
}
}
onMouseMove(event) {
var diagramEngine = this.props.diagramEngine;
var diagramModel = diagramEngine.getDiagramModel();
//select items so draw a bounding box
if (this.state.action instanceof SelectingAction) {
var relative = diagramEngine.getRelativePoint(event.clientX, event.clientY);
_.forEach(diagramModel.getNodes(), node => {
if ((this.state.action as SelectingAction).containsElement(node.x, node.y, diagramModel)) {
node.setSelected(true);
}
});
_.forEach(diagramModel.getLinks(), link => {
var allSelected = true;
_.forEach(link.points, point => {
if ((this.state.action as SelectingAction).containsElement(point.x, point.y, diagramModel)) {
point.setSelected(true);
} else {
allSelected = false;
}
});
if (allSelected) {
link.setSelected(true);
}
});
this.state.action.mouseX2 = relative.x;
this.state.action.mouseY2 = relative.y;
this.fireAction();
this.setState({ action: this.state.action });
return;
} else if (this.state.action instanceof MoveItemsAction) {
let amountX = event.clientX - this.state.action.mouseX;
let amountY = event.clientY - this.state.action.mouseY;
let amountZoom = diagramModel.getZoomLevel() / 100;
_.forEach(this.state.action.selectionModels, model => {
// in this case we need to also work out the relative grid position
if (
model.model instanceof NodeModel ||
(model.model instanceof PointModel && !model.model.isConnectedToPort())
) {
model.model.x = diagramModel.getGridPosition(model.initialX + amountX / amountZoom);
model.model.y = diagramModel.getGridPosition(model.initialY + amountY / amountZoom);
// update port coordinates as well
if (model.model instanceof NodeModel) {
_.forEach(model.model.getPorts(), port => {
const portCoords = this.props.diagramEngine.getPortCoords(port);
port.updateCoords(portCoords);
});
}
if (diagramEngine.isSmartRoutingEnabled()) {
diagramEngine.calculateRoutingMatrix();
}
} else if (model.model instanceof PointModel) {
// we want points that are connected to ports, to not neccesarilly snap to grid
// this stuff needs to be pixel perfect, dont touch it
model.model.x = model.initialX + diagramModel.getGridPosition(amountX / amountZoom);
model.model.y = model.initialY + diagramModel.getGridPosition(amountY / amountZoom);
}
});
if (diagramEngine.isSmartRoutingEnabled()) {
diagramEngine.calculateCanvasMatrix();
}
this.fireAction();
if (!this.state.wasMoved) {
this.setState({ wasMoved: true });
} else {
this.forceUpdate();
}
} else if (this.state.action instanceof MoveCanvasAction) {
//translate the actual canvas
if (this.props.allowCanvasTranslation) {
diagramModel.setOffset(
this.state.action.initialOffsetX + (event.clientX - this.state.action.mouseX),
this.state.action.initialOffsetY + (event.clientY - this.state.action.mouseY)
);
this.fireAction();
this.forceUpdate();
}
}
}
onKeyUp(event) {
//delete all selected
if (this.props.deleteKeys.indexOf(event.keyCode) !== -1) {
_.forEach(this.props.diagramEngine.getDiagramModel().getSelectedItems(), element => {
//only delete items which are not locked
if (!this.props.diagramEngine.isModelLocked(element)) {
element.remove();
}
});
this.forceUpdate();
}
}
onMouseUp(event) {
var diagramEngine = this.props.diagramEngine;
//are we going to connect a link to something?
if (this.state.action instanceof MoveItemsAction) {
var element = this.getMouseElement(event);
var linkConnected = false;
_.forEach(this.state.action.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
if (element && element.model instanceof PortModel && !diagramEngine.isModelLocked(element.model)) {
linkConnected = true;
let link = model.model.getLink();
if (link.getTargetPort() !== null) {
//if this was a valid link already and we are adding a node in the middle, create 2 links from the original
if (link.getTargetPort() !== element.model && link.getSourcePort() !== element.model) {
const targetPort = link.getTargetPort();
let newLink = link.clone({});
newLink.setSourcePort(element.model);
newLink.setTargetPort(targetPort);
link.setTargetPort(element.model);
targetPort.removeLink(link);
newLink.removePointsBefore(newLink.getPoints()[link.getPointIndex(model.model)]);
link.removePointsAfter(model.model);
diagramEngine.getDiagramModel().addLink(newLink);
//if we are connecting to the same target or source, remove tweener points
} else if (link.getTargetPort() === element.model) {
link.removePointsAfter(model.model);
} else if (link.getSourcePort() === element.model) {
link.removePointsBefore(model.model);
}
} else {
link.setTargetPort(element.model);
}
delete this.props.diagramEngine.linksThatHaveInitiallyRendered[link.getID()];
}
//if we moved a NodeModel and allowLooseLinks is false, we know that any links involved were valid
if ((!this.props.allowLooseLinks && element.model instanceof NodeModel) || !this.state.wasMoved) {
linkConnected = true;
}
});
//do we want to allow loose links on the diagram model or not
if (!linkConnected && !this.props.allowLooseLinks) {
_.forEach(this.state.action.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
var link = model.model.getLink();
if (link.isLastPoint(model.model)) {
link.remove();
}
});
}
//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();
this.stopFiringAction(!this.state.wasMoved);
} else {
diagramEngine.clearRepaintEntities();
this.stopFiringAction();
}
this.state.document.removeEventListener("mousemove", this.onMouseMove);
this.state.document.removeEventListener("mouseup", this.onMouseUp);
}
drawSelectionBox() {
let dimensions = (this.state.action as SelectingAction).getBoxDimensions();
return (
<div
className={this.bem("__selector")}
style={{
top: dimensions.top,
left: dimensions.left,
width: dimensions.width,
height: dimensions.height
}}
/>
);
}
render() {
var diagramEngine = this.props.diagramEngine;
diagramEngine.setMaxNumberPointsPerLink(this.props.maxNumberPointsPerLink);
diagramEngine.setSmartRoutingStatus(this.props.smartRouting);
var diagramModel = diagramEngine.getDiagramModel();
return (
<div
{...this.getProps()}
ref={ref => {
if (ref) {
this.props.diagramEngine.setCanvas(ref);
}
}}
onWheel={event => {
if (this.props.allowCanvasZoom) {
event.preventDefault();
event.stopPropagation();
const oldZoomFactor = diagramModel.getZoomLevel() / 100;
let scrollDelta = this.props.inverseZoom ? -event.deltaY : event.deltaY;
//check if it is pinch gesture
if (event.ctrlKey && scrollDelta % 1 !== 0) {
/*Chrome and Firefox sends wheel event with deltaY that
have fractional part, also `ctrlKey` prop of the event is true
though ctrl isn't pressed
*/
scrollDelta /= 3;
} else {
scrollDelta /= 60;
}
if (diagramModel.getZoomLevel() + scrollDelta > 10) {
diagramModel.setZoomLevel(diagramModel.getZoomLevel() + scrollDelta);
}
const zoomFactor = diagramModel.getZoomLevel() / 100;
const boundingRect = event.currentTarget.getBoundingClientRect();
const clientWidth = boundingRect.width;
const clientHeight = boundingRect.height;
// compute difference between rect before and after scroll
const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor;
const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor;
// compute mouse coords relative to canvas
const clientX = event.clientX - boundingRect.left;
const clientY = event.clientY - boundingRect.top;
// compute width and height increment factor
const xFactor = (clientX - diagramModel.getOffsetX()) / oldZoomFactor / clientWidth;
const yFactor = (clientY - diagramModel.getOffsetY()) / oldZoomFactor / clientHeight;
diagramModel.setOffset(
diagramModel.getOffsetX() - widthDiff * xFactor,
diagramModel.getOffsetY() - heightDiff * yFactor
);
diagramEngine.enableRepaintEntities([]);
this.forceUpdate();
}
}}
onMouseDown={event => {
this.setState({ ...this.state, wasMoved: false });
diagramEngine.clearRepaintEntities();
var model = this.getMouseElement(event);
//the canvas was selected
if (model === null) {
//is it a multiple selection
if (event.shiftKey) {
var relative = diagramEngine.getRelativePoint(event.clientX, event.clientY);
this.startFiringAction(new SelectingAction(relative.x, relative.y));
} else {
//its a drag the canvas event
diagramModel.clearSelection();
this.startFiringAction(new MoveCanvasAction(event.clientX, event.clientY, diagramModel));
}
} else if (model.model instanceof PortModel) {
//its a port element, we want to drag a link
if (!this.props.diagramEngine.isModelLocked(model.model)) {
var relative = diagramEngine.getRelativeMousePoint(event);
var sourcePort = model.model;
var link = sourcePort.createLinkModel();
link.setSourcePort(sourcePort);
if (link) {
link.removeMiddlePoints();
if (link.getSourcePort() !== sourcePort) {
link.setSourcePort(sourcePort);
}
link.setTargetPort(null);
link.getFirstPoint().updateLocation(relative);
link.getLastPoint().updateLocation(relative);
diagramModel.clearSelection();
link.getLastPoint().setSelected(true);
diagramModel.addLink(link);
this.startFiringAction(
new MoveItemsAction(event.clientX, event.clientY, diagramEngine)
);
}
} else {
diagramModel.clearSelection();
}
} else {
//its some or other element, probably want to move it
if (!event.shiftKey && !model.model.isSelected()) {
diagramModel.clearSelection();
}
model.model.setSelected(true);
this.startFiringAction(new MoveItemsAction(event.clientX, event.clientY, diagramEngine));
}
this.state.document.addEventListener("mousemove", this.onMouseMove);
this.state.document.addEventListener("mouseup", this.onMouseUp);
}}
>
{this.state.renderedNodes && (
<LinkLayerWidget
diagramEngine={diagramEngine}
pointAdded={(point: PointModel, event) => {
this.state.document.addEventListener("mousemove", this.onMouseMove);
this.state.document.addEventListener("mouseup", this.onMouseUp);
event.stopPropagation();
diagramModel.clearSelection(point);
this.setState({
action: new MoveItemsAction(event.clientX, event.clientY, diagramEngine)
});
}}
/>
)}
<NodeLayerWidget diagramEngine={diagramEngine} />
{this.state.action instanceof SelectingAction && this.drawSelectionBox()}
</div>
);
}
}

View File

@@ -1,30 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "../models/LinkModel";
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
export interface LinkProps extends BaseWidgetProps {
link: LinkModel;
diagramEngine: DiagramEngine;
children?: any;
}
export interface LinkState {}
/**
* @author Dylan Vorster
*/
export class LinkWidget extends BaseWidget<LinkProps, LinkState> {
constructor(props: LinkProps) {
super("srd-link", props);
this.state = {};
}
shouldComponentUpdate() {
return this.props.diagramEngine.canEntityRepaint(this.props.link);
}
render() {
return this.props.children;
}
}

View File

@@ -1,8 +1,7 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import { NodeModel } from "../models/NodeModel";
import { Toolkit } from "../Toolkit";
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface NodeProps extends BaseWidgetProps {
node: NodeModel;
@@ -12,19 +11,12 @@ export interface NodeProps extends BaseWidgetProps {
export interface NodeState {}
/**
* @author Dylan Vorster
*/
export class NodeWidget extends BaseWidget<NodeProps, NodeState> {
constructor(props: NodeProps) {
super("srd-node", props);
this.state = {};
}
shouldComponentUpdate() {
return this.props.diagramEngine.canEntityRepaint(this.props.node);
}
getClassName() {
return "node " + super.getClassName() + (this.props.node.isSelected() ? this.bem("--selected") : "");
}
@@ -33,10 +25,10 @@ export class NodeWidget extends BaseWidget<NodeProps, NodeState> {
return (
<div
{...this.getProps()}
data-nodeid={this.props.node.id}
data-nodeid={this.props.node.getID()}
style={{
top: this.props.node.y,
left: this.props.node.x
top: this.props.node.getDimensions().getTopLeft().y,
left: this.props.node.getDimensions().getTopLeft().x
}}
>
{this.props.children}

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { NodeModel } from "../models/NodeModel";
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface PortProps extends BaseWidgetProps {
name: string;
@@ -11,9 +11,6 @@ export interface PortState {
selected: boolean;
}
/**
* @author Dylan Vorster
*/
export class PortWidget extends BaseWidget<PortProps, PortState> {
constructor(props: PortProps) {
super("srd-port", props);

View File

@@ -1,92 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../../DiagramEngine";
import { LinkWidget } from "../LinkWidget";
import * as _ from "lodash";
import { PointModel } from "../../models/PointModel";
import { BaseWidget, BaseWidgetProps } from "../BaseWidget";
export interface LinkLayerProps extends BaseWidgetProps {
diagramEngine: DiagramEngine;
pointAdded: (point: PointModel, event: MouseEvent) => any;
}
export interface LinkLayerState {}
/**
* @author Dylan Vorster
*/
export class LinkLayerWidget extends BaseWidget<LinkLayerProps, LinkLayerState> {
constructor(props: LinkLayerProps) {
super("srd-link-layer", props);
this.state = {};
}
render() {
var diagramModel = this.props.diagramEngine.getDiagramModel();
return (
<svg
{...this.getProps()}
style={{
transform:
"translate(" +
diagramModel.getOffsetX() +
"px," +
diagramModel.getOffsetY() +
"px) scale(" +
diagramModel.getZoomLevel() / 100.0 +
")"
}}
>
{//only perform these actions when we have a diagram
this.props.diagramEngine.canvas &&
_.map(diagramModel.getLinks(), link => {
if (
this.props.diagramEngine.nodesRendered &&
!this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id]
) {
if (link.sourcePort !== null) {
try {
const portCenter = this.props.diagramEngine.getPortCenter(link.sourcePort);
link.points[0].updateLocation(portCenter);
const portCoords = this.props.diagramEngine.getPortCoords(link.sourcePort);
link.sourcePort.updateCoords(portCoords);
this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id] = true;
} catch (ignore) {
/*noop*/
}
}
if (link.targetPort !== null) {
try {
const portCenter = this.props.diagramEngine.getPortCenter(link.targetPort);
_.last(link.points).updateLocation(portCenter);
const portCoords = this.props.diagramEngine.getPortCoords(link.targetPort);
link.targetPort.updateCoords(portCoords);
this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id] = true;
} catch (ignore) {
/*noop*/
}
}
}
//generate links
var generatedLink = this.props.diagramEngine.generateWidgetForLink(link);
if (!generatedLink) {
throw new Error(`no link generated for type: ${link.getType()}`);
}
return (
<LinkWidget key={link.getID()} link={link} diagramEngine={this.props.diagramEngine}>
{React.cloneElement(generatedLink, {
pointAdded: this.props.pointAdded
})}
</LinkWidget>
);
})}
</svg>
);
}
}

View File

@@ -1,64 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../../DiagramEngine";
import * as _ from "lodash";
import { NodeWidget } from "../NodeWidget";
import { NodeModel } from "../../models/NodeModel";
import { BaseWidget, BaseWidgetProps } from "../BaseWidget";
export interface NodeLayerProps extends BaseWidgetProps {
diagramEngine: DiagramEngine;
}
export interface NodeLayerState {}
export class NodeLayerWidget extends BaseWidget<NodeLayerProps, NodeLayerState> {
constructor(props: NodeLayerProps) {
super("srd-node-layer", props);
this.state = {};
}
updateNodeDimensions = () => {
if (!this.props.diagramEngine.nodesRendered) {
const diagramModel = this.props.diagramEngine.getDiagramModel();
_.map(diagramModel.getNodes(), node => {
node.updateDimensions(this.props.diagramEngine.getNodeDimensions(node));
});
}
};
componentDidUpdate() {
this.updateNodeDimensions();
this.props.diagramEngine.nodesRendered = true;
}
render() {
var diagramModel = this.props.diagramEngine.getDiagramModel();
return (
<div
{...this.getProps()}
style={{
transform:
"translate(" +
diagramModel.getOffsetX() +
"px," +
diagramModel.getOffsetY() +
"px) scale(" +
diagramModel.getZoomLevel() / 100.0 +
")"
}}
>
{_.map(diagramModel.getNodes(), (node: NodeModel) => {
return React.createElement(
NodeWidget,
{
diagramEngine: this.props.diagramEngine,
key: node.id,
node: node
},
this.props.diagramEngine.generateWidgetForNode(node)
);
})}
</div>
);
}
}

View File

@@ -58,6 +58,25 @@ export class E2EPort extends E2EElement {
_.difference(_.flatMap((await this.parent.model()).ports, "links"), currentLinks)[0]
);
}
async linkToPoint(x: number, y: number): Promise<E2ELink> {
let currentLinks = _.flatMap((await this.parent.model()).ports, "links");
let bounds = await this.element.boundingBox();
// click on this port
this.page.mouse.move(bounds.x, bounds.y);
this.page.mouse.down();
// drag to point
this.page.mouse.move(x, y);
this.page.mouse.up();
// get the parent to get the link
return await this.helper.link(
_.difference(_.flatMap((await this.parent.model()).ports, "links"), currentLinks)[0]
);
}
}
export class E2ELink extends E2EElement {

View File

@@ -0,0 +1,60 @@
import "jest";
import * as puppeteer from "puppeteer";
import { E2EHelper } from "./E2EHelper";
var browser;
async function itShould(demo: string, directive, test: (page: puppeteer.Page, helper: E2EHelper) => any) {
it(directive, async () => {
let page = await browser.newPage();
await page.goto("file://" + __dirname + "/../../dist/e2e/" + demo + "/index.html");
let helper = new E2EHelper(page);
await test(page, helper);
await page.close();
});
}
beforeAll(async () => {
if (process.env.CIRCLECI) {
console.log("using CircleCI");
browser = await puppeteer.launch({
args: ["--no-sandbox", "--disable-setuid-sandbox"]
});
} else {
browser = await puppeteer.launch({
headless: false
});
}
});
afterAll(() => {
browser.close();
});
describe("simple flow test", async () => {
itShould("demo-simple-flow", "drag link to port adds a link", async (page, helper) => {
// create a new link
let node1 = await helper.node("6");
let node2 = await helper.node("9");
let port1 = await node1.port("7");
let port2 = await node2.port("10");
let newlink = await port1.link(port2);
await expect(await newlink.exists()).toBeTruthy();
});
itShould("demo-simple-flow", "drag link to node does not add a link", async (page, helper) => {
// create a new link
let node1 = await helper.node("6");
let node2 = await helper.node("9");
let port1 = await node1.port("7");
let node2Bounds = await node2.element.boundingBox();
let newlink = await port1.linkToPoint(node2Bounds.x, node2Bounds.y);
await expect(await newlink.exists()).toBeFalsy();
});
});