Compare commits

..

42 Commits

Author SHA1 Message Date
Dylan Vorster
e333a4f168 correct version 2019-07-22 22:59:31 +02:00
Dylan Vorster
1d316409e1 npm 2019-07-22 22:57:10 +02:00
Dylan Vorster
9d947d40b7 5.3.1 2019-07-22 22:55:55 +02:00
Dylan Vorster
7d80f15358 improvements 2019-07-22 22:55:33 +02:00
Dylan Vorster
28844b86eb 5.3.0 2019-07-22 22:48:40 +02:00
Dylan Vorster
70c9aeac66 please 2 2019-07-22 22:44:15 +02:00
Dylan Vorster
12a7bfbda5 please 2019-07-22 21:58:46 +02:00
Dylan Vorster
650061db4a fix jest 2019-07-22 21:05:28 +02:00
Dylan Vorster
48c6d1f038 changelog 2019-07-22 20:42:53 +02:00
Dylan Vorster
7cea7032f0 5.2.2-0 2019-07-22 18:49:46 +02:00
Dylan Vorster
2b346aa326 Merge branch 'master' of https://github.com/svssrinivas/react-diagrams into 5.3 2019-07-22 18:44:43 +02:00
Dylan Vorster
0f7f8853d1 docs 2019-07-22 18:43:53 +02:00
Dylan Vorster
3a57ea711e docs 2019-07-22 18:41:11 +02:00
Dylan Vorster
de208e3250 fixes and such 2019-07-22 18:37:17 +02:00
Dylan Vorster
2df09edb7e Merge pull request #331 from sahilit/patch-1
typo error
2019-07-22 16:39:08 +02:00
Dylan Vorster
ba1401a8f2 Merge pull request #356 from frank-martinez-27/5.2.0
Fixed "Type 'PointModel[]' is not assignable to type 'ConcatArray<BaseModel<any, T>>'" error
2019-07-22 16:38:44 +02:00
Francisco
c6137d03ce Fixed broken snapshots 2019-06-27 19:27:02 -07:00
Francisco
ead43957e3 Fixed 'Return type of public method from exported class has or is using name PointModel from external module' error 2019-06-27 19:21:29 -07:00
Francisco
b7d8ffa793 First commit 2019-06-27 19:08:05 -07:00
Srinivas2794
a289c73b68 fix line direction while maintaining smoothening 2019-05-26 18:26:04 +05:30
Sahil Singh
b75976d091 Update Getting Started.md 2019-03-18 13:02:39 +05:30
Dylan Vorster
a95010652d Merge pull request #178 from smeijer/feature/add-position-changed-event
feat(events): add `positionChanged` event to `NodeModel`
2019-02-28 22:08:23 +02:00
Dylan Vorster
4cd24940c8 Merge pull request #305 from JoasE/master
Fixed a small positioning issue with dagre demo
2018-12-02 00:40:07 +02:00
Dylan Vorster
0f1dec1ebc Merge pull request #299 from igauravsehrawat/typo-fix
--fix: Typo.
2018-12-02 00:39:29 +02:00
Dylan Vorster
eedc21cc87 Merge pull request #306 from thepocp/change-link-extras-type
Impossible to set extra properties
2018-12-02 00:39:13 +02:00
JoasE
b719178f44 Fixed up storybook snapshot 2018-12-01 17:30:41 +01:00
Mihail Novikov
12724748e6 Impossible to set extra porperties 2018-11-30 17:29:16 +03:00
JoasE
0ebd6d918f Ran prettier 2018-11-30 14:24:51 +01:00
JoasE
591ec2e76e Fixed a small positioning issue with dagre demo
The dagre example doesn't take into account that dagre works with center of position for nodes but react-diagrams works with top left of position for nodes.

To correctly position the nodes, half of the width and height should be subtracted from the x and y axis respectively.
2018-11-30 14:12:24 +01:00
G
9f95ac8451 --fix: Typo. 2018-10-31 20:55:10 +05:30
Dylan Vorster
e3fc996092 Actually fix it this time lol 2018-08-01 22:57:43 +02:00
Dylan Vorster
de9765358f Fix Demo URL 2018-08-01 22:57:19 +02:00
Dylan Vorster
fa34f5c98b Merge pull request #259 from brian-eightsolutions/patch-1
Update DiagramWidget.tsx
2018-07-13 13:28:15 +02:00
brian-eightsolutions
f4ac467056 Update DiagramWidget.tsx
This is the proposed fix for the issue that I just logged: https://github.com/projectstorm/react-diagrams/issues/258
2018-07-10 12:42:03 -07:00
Dylan Vorster
5b142c1c61 update changelog 2018-05-05 18:00:41 +02:00
Dylan Vorster
40219d7a4d 5.2.1 2018-05-05 17:52:31 +02:00
Dylan Vorster
0eb432b302 lol 2018-05-05 17:52:27 +02:00
Dylan Vorster
ad5e7a4b4d 5.2.0 2018-05-05 17:49:04 +02:00
Dylan Vorster
0583db1c74 bump all the things, fix small issue 2018-05-05 17:44:31 +02:00
Dylan Vorster
0b92e939ab Merge pull request #236 from ajthinking/patch-2
Added notes to example images
2018-05-01 12:18:44 +02:00
Anders Jürisoo
8997eba4b4 Added notes to example images
See #157, #56
2018-05-01 12:13:35 +02:00
Stephan Meijer
3fbbeb82de feat(events): add positionChanged event to NodeModel 2018-03-01 22:01:52 +01:00
93 changed files with 10650 additions and 7433 deletions

3
.babelrc Normal file
View File

@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@@ -1,6 +1,6 @@
[*]
indent_style = tab
indent_size = 4
indent_size = 2
trim_trailing_whitespace = true
# Some exceptions

200
.gitignore vendored
View File

@@ -1,194 +1,8 @@
dist/
dist/main.js
dist/main.js.map
/package
*.tgz
@types/
.out
# Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node
#!! ERROR: net is undefined. Use list command to see defined gitignore types !!#
### NetBeans ###
nbproject/private/
build/
nbbuild/
nbdist/
nbactions.xml
.nb-gradle/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### PhpStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
dist
.DS_Store
.idea
.out
*.zip
.env
node_modules
yarn-error.log

View File

@@ -1,194 +1,4 @@
demos
images
docs
.out
.storybook
.circleci
tests
*.md
# Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node
#!! ERROR: net is undefined. Use list command to see defined gitignore types !!#
### NetBeans ###
nbproject/private/
build/
nbbuild/
nbdist/
nbactions.xml
.nb-gradle/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### PhpStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
.idea
*
!dist/**/*
!package.json
!README.md

View File

@@ -1,14 +0,0 @@
import React from 'react';
import addons from '@storybook/addons';
export class WithCode extends React.Component {
render() {
const { children, code } = this.props;
const channel = addons.getChannel();
// send the notes to the channel.
channel.emit('storybook/code/set_code', code);
// return children elements.
return children;
}
}

View File

@@ -1,69 +0,0 @@
import React from 'react';
import addons from '@storybook/addons';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { github } from 'react-syntax-highlighter/styles/hljs';
/**
* @author Dylan
*
* Simple little addon for displaying code, might make this a seperate project at some point
*/
export class CodePreview extends React.Component {
constructor(...args) {
super(...args);
this.state = {
code: ''
};
}
componentDidMount() {
const { channel, api } = this.props;
// Listen to the notes and render it.
channel.on('storybook/code/set_code', (code) => {
this.onAddCode(code);
});
// Clear the current notes on every story change.
this.stopListeningOnStory = api.onStory(() => {
this.onAddCode('');
});
}
// This is some cleanup tasks when the Notes panel is unmounting.
componentWillUnmount() {
if (this.stopListeningOnStory) {
this.stopListeningOnStory();
}
this.unmounted = true;
const { channel } = this.props;
channel.removeListener('storybook/notes/add_notes', this.onAddCode);
}
onAddCode(code) {
this.setState({ code: code });
}
render() {
return (
<SyntaxHighlighter
customStyle={{width: '100%', overflowX:'hidden', tabSize: 4}}
showLineNumbers={true}
language='language-tsx'
style={github}
>
{this.state.code}
</SyntaxHighlighter>
);
}
}
// Register the addon with a unique name.
addons.register('storybook/code', api => {
// Also need to set a unique name to the panel.
addons.addPanel('storybook/code/panel', {
title: 'Code preview',
render: () => <CodePreview channel={addons.getChannel()} api={api} />,
});
});

View File

@@ -1,3 +1,2 @@
import './addon-code/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-options/register';

View File

@@ -1,39 +1,48 @@
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
loaders: ["style-loader", "css-loader", "sass-loader"],
include: path.resolve(__dirname, '../')
},
{
test: /\.css/,
loaders: ["style-loader", "css-loader"],
include: path.resolve(__dirname, '../')
},
{
enforce: 'pre',
test: /\.js$/,
loader: "source-map-loader",
exclude: [
/node_modules\//
]
},
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader?declaration=false',
},
{
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
loader: "file-loader"
module.exports = async ({config, mode}) => {
return {
...config,
resolve: {
...config.resolve,
extensions: ['.tsx', '.ts', '.js'],
alias: {
...config.resolve.alias,
'storm-react-diagrams': path.join(__dirname, "..", "src", "main")
}
]
},
resolve: {
alias: {
'storm-react-diagrams': path.join(__dirname, "..", "src", "main")
},
extensions: [".tsx", ".ts", ".js"]
}
module: {
...config.module,
rules: [
...config.module.rules,
...[
{
test: /\.scss$/,
loaders: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {config: {path: path.join(__dirname, '..')}}
},
'sass-loader'
]
},
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
exclude: [/node_modules\//]
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
options: {
transpileOnly: true
}
},
]
]
}
};
};

View File

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

View File

@@ -1,3 +1,22 @@
__5.3.0__
* [maintenance] Upgrade :allthethings: (all the build tooling was upgrade)
* [api] move to ES6 (JS now contains native classes)
* [api] changed package name to @projectstorm/react-diagrams
* [bug] [PR259](https://github.com/projectstorm/react-diagrams/pull/259) Fixes #258
* [refactor] [PR 306](https://github.com/projectstorm/react-diagrams/pull/306) `:any` fix
* [feature] [PR 178](https://github.com/projectstorm/react-diagrams/pull/178) Trigger a positionChanged event when moving a Node that has the listener assigned.
* [fix] [PR 356](https://github.com/projectstorm/react-diagrams/pull/356) Fixed Type issue with 'PointModel[]'
* [demo] dark mode and upgrade storybook
__5.2.1__
* [fix] Always remove link from old source/target port on port change
* [maintenance] upgrade node modules
* [refactor] https://github.com/projectstorm/react-diagrams/commit/55f62587bd3b12513c7d37eff59edfc8bdb8d6c9
* [bug] https://github.com/projectstorm/react-diagrams/commit/75ef02dd4d131a0e7c08b2680c69efc390e50b84
-> and other improvements, also checkout the foundation work happening over at https://github.com/projectstorm/react-canvas
__5.1.0__
* [api] Rename XXXFactory into AbstractXXXFactory

View File

@@ -1,12 +1,14 @@
# 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).
__PSA 2018__: React Diagrams ~is currently~ was 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).
__PSA 2019__: I still want to jump onto the rewrite, but it is a much larger project than anticipated, so going to try maintain this one in the mean time.
---
**DEMO**: [http://www.projectstorm.io/react-diagrams](http://www.projectstorm.io/react-diagrams)
**DEMO**: [http://projectstorm.cloud/react-diagrams](http://projectstorm.cloud/react-diagrams)
**DOCS:** [https://projectstorm.gitbooks.io/react-diagrams](https://projectstorm.gitbooks.io/react-diagrams)
**(SOME) DOCS:** [https://projectstorm.gitbooks.io/react-diagrams](https://projectstorm.gitbooks.io/react-diagrams)
**RELEASE NOTES** : [http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/](http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/)
@@ -14,12 +16,13 @@ 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) [![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)
Example implementation using custom models:
![Personal Project](./images/example1.jpg)
![](./images/example2.jpg)
Get started with the default models right out of the box:
![](./images/example3.jpg)
## Introduction
A no-nonsense diagramming library written entirely in React with the help of a few small libraries. It aims to be:
@@ -29,7 +32,11 @@ A no-nonsense diagramming library written entirely in React with the help of a f
* Simple to operate and understand without sugar and magic
* Fast and optimized to handle large diagrams with hundreds of nodes/links
* Super easy to use, and should work as you expect it to
* Perfect for creating declarative systems such as programmatic pipelines and visual programming languages
* Perfect for creating declarative systems such as programmatic pipelines and visual programming languages
#### Installing
```yarn add @projectstorm/react-diagrams```
#### Run the demos
@@ -37,7 +44,7 @@ After running `yarn install` you must then run: `yarn run storybook`
#### Building from source
Simply run `webpack` in the root directory \(or `export NODE_ENV=production && webpack` if you want a production build\) and it will spit out the transpiled code and typescript definitions into the dist directory as a single file.
Simply run `webpack` in the root directory \(or `export NODE_ENV=production && webpack` if you want a production build\) and it will spit out the transpiled code and typescript definitions into the dist directory as a single file.
We use webpack for this because TSC cannot compile a single UMD file \(TSC can currently only output multiple UMD files\).
## [Checkout the docs](https://projectstorm.gitbooks.io/react-diagrams)

View File

@@ -1,6 +1,4 @@
import * as React from "react";
import { withDocs } from "storybook-readme";
import { WithCode } from "../../.storybook/addon-code/react.js";
export class Helper {
/**
@@ -22,16 +20,4 @@ export class Helper {
console.log(event.clientX, event.clientY);
});
}
static makeDemo(widget, code, markdown?) {
let container = () => <WithCode code={code}>{widget}</WithCode>;
if (markdown) {
return withDocs({
PreviewComponent: ({ children }) => {
return <div className="docs-preview-wrapper">{children}</div>;
}
})(markdown, container);
}
return container;
}
}

View File

@@ -1,5 +1,11 @@
@import "../../src/sass/main";
html, body, #root{
height: 100%;
padding: 0;
margin: 0;
}
.srd-demo-workspace{
background: black;
display: flex;
@@ -36,15 +42,6 @@
}
}
.docs-preview-wrapper{
background: rgb(60,60,60);
border-radius: 10px;
overflow: hidden;
padding: 10px;
margin-top: 20px;
margin-bottom: 20px;
}
.srd-demo-canvas{
height: 100%;
min-height: 300px;

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.installDefaults();
engine.installDefaultFactories();
//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.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
engine.registerLinkFactory(new AdvancedLinkFactory());
// create some nodes
@@ -150,7 +150,7 @@ export default () => {
model.addAll(node1, node2, node3, node4);
// load model into engine
engine.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
// 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.setModel(model);
engine.setDiagramModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -11,8 +11,8 @@ export function distributeElements(model) {
let nodes = distributeGraph(clonedModel);
nodes.forEach(node => {
let modelNode = clonedModel.nodes.find(item => item.id === node.id);
modelNode.x = node.x;
modelNode.y = node.y;
modelNode.x = node.x - node.width / 2;
modelNode.y = node.y - node.height / 2;
});
return clonedModel;
}

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.setModel(distributedModel);
engine.setDiagramModel(distributedModel);
this.forceUpdate();
}
@@ -68,7 +68,7 @@ function getDistributedModel(engine, model) {
export default () => {
//1) setup the diagram engine
let engine = new DiagramEngine();
engine.installDefaults();
engine.installDefaultFactories();
//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.setModel(model2);
engine.setDiagramModel(model2);
return <Demo8Widget engine={engine} />;
};

View File

@@ -9,14 +9,14 @@ export class Application {
constructor() {
this.diagramEngine = new SRD.DiagramEngine();
this.diagramEngine.installDefaults();
this.diagramEngine.installDefaultFactories();
this.newModel();
}
public newModel() {
this.activeModel = new SRD.DiagramModel();
this.diagramEngine.setModel(this.activeModel);
this.diagramEngine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
//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.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
// setup the diagram model
const model = new DiagramModel();
@@ -46,14 +46,13 @@ export default () => {
// also a label for A and D
const link3 = port1.link(port4);
link3.setTargetPort(port4);
(link3 as DefaultLinkModel).addLabel("Emoji label: 🎉");
// add all to the main model
model.addAll(node1, node2, node3, node4, link1, link2, link3);
// load model into engine and render
engine.setModel(model);
engine.setDiagramModel(model);
return (
<DemoWorkspaceWidget

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaults();
engine.installDefaultFactories();
var model = new DiagramModel();
@@ -51,7 +51,7 @@ export default () => {
model.addAll(node3, node4, link2);
engine.setModel(model);
engine.setDiagramModel(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.setModel(model2);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
//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.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
//2) setup the diagram model
var model = new DiagramModel();
@@ -23,7 +23,7 @@ export default () => {
}
//5) load model into engine
engine.setModel(model);
engine.setDiagramModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;

View File

@@ -1,14 +1,13 @@
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel } from "storm-react-diagrams";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } 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.installDefaults();
engine.installDefaultFactories();
//2) setup the diagram model
var model = new DiagramModel();
@@ -30,17 +29,17 @@ export default () => {
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setModel(model);
engine.setDiagramModel(model);
//!------------- SERIALIZING ------------------
var str = JSON.stringify(model.serialize());
var str = JSON.stringify(model.serializeDiagram());
//!------------- DESERIALIZING ----------------
var model2 = new DiagramModel();
model2.deSerialize(new DeserializeEvent(JSON.parse(str), engine));
engine.setModel(model2);
model2.deSerializeDiagram(JSON.parse(str), engine);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
//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.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
//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.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
// 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.setModel(model);
engine.setDiagramModel(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.installDefaults();
engine.installDefaultFactories();
//2) setup the diagram model
var model = new DiagramModel();
@@ -30,13 +30,11 @@ export default () => {
}
//5) load model into engine
engine.setModel(model);
engine.setDiagramModel(model);
//6) render the diagram!
return (
<DemoWorkspaceWidget
buttons={<button onClick={() => engine.getCanvasWidget().zoomToFit()}>Zoom to fit</button>}
>
<DemoWorkspaceWidget buttons={<button onClick={() => engine.zoomToFit()}>Zoom to fit</button>}>
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);

View File

@@ -1,23 +1,21 @@
import * as React from "react";
import { storiesOf, addDecorator } from "@storybook/react";
import { setOptions } from "@storybook/addon-options";
import { host } from "storybook-host";
import { Helper } from "./.helpers/Helper";
import { Toolkit } from "../src/Toolkit";
//include the SCSS for the demo
import "./.helpers/demo.scss";
import {storiesOf, addDecorator, addParameters} from "@storybook/react";
import {setOptions} from "@storybook/addon-options";
import {host} from "storybook-host";
import {Helper} from "./.helpers/Helper";
import {Toolkit} from "../src/Toolkit";
import {themes} from '@storybook/theming';
Toolkit.TESTING = true;
addDecorator(
host({
cropMarks: false,
height: "100%",
width: "100%",
padding: 20
})
);
addParameters({
options: {
theme: themes.dark,
},
});
//include the SCSS for the demo
import "./.helpers/demo.scss";
setOptions({
name: "STORM React Diagrams",
@@ -25,105 +23,51 @@ setOptions({
addonPanelInRight: true
});
import demo_simple from "./demo-simple";
import demo_flow from "./demo-simple-flow";
import demo_performance from "./demo-performance";
import demo_locks from "./demo-locks";
import demo_grid from "./demo-grid";
import demo_limit_points from "./demo-limit-points";
import demo_listeners from "./demo-listeners";
import demo_zoom from "./demo-zoom-to-fit";
import demo_labels from "./demo-labelled-links";
storiesOf("Simple Usage", module)
.add(
"Simple example",
Helper.makeDemo(
require("./demo-simple/index").default(),
require("!!raw-loader!./demo-simple/index"),
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 example", demo_simple)
.add("Simple flow example", demo_flow)
.add("Performance demo", demo_performance)
.add("Locked widget", demo_locks)
.add("Canvas grid size", demo_grid)
.add("Limiting link points", demo_limit_points)
.add("Events and listeners", demo_listeners)
.add("Zoom to fit", demo_zoom)
.add("Links with labels", demo_labels);
import demo_adv_clone_selected from "./demo-cloning";
import demo_adv_ser_des from "./demo-serializing";
import demo_adv_prog from "./demo-mutate-graph";
import demo_adv_dnd from "./demo-drag-and-drop";
import demo_smart_routing from "./demo-smart-routing";
storiesOf("Advanced Techniques", module)
.add("Clone Selected", demo_adv_clone_selected)
.add("Serializing and de-serializing", demo_adv_ser_des)
.add("Programatically modifying graph", demo_adv_prog)
.add("Drag and drop", demo_adv_dnd)
.add("Smart routing", demo_smart_routing);
import demo_cust_nodes from "./demo-custom-node1";
import demo_cust_links from "./demo-custom-link1";
storiesOf("Custom Models", module)
.add("Custom diamond node", demo_cust_nodes)
.add("Custom animated links", demo_cust_links);
import demo_3rd_dagre from "./demo-dagre";
storiesOf("3rd party libraries", module)
.add("Auto Distribute (Dagre)", demo_3rd_dagre);
// enable this to log mouse location when writing new puppeteer tests
//Helper.logMousePosition()

View File

@@ -7,7 +7,7 @@ The first thing you need to do, is grab the distribution files on NPM. You can d
**Via yarn:**
```
yarn install storm-react-diagrams
yarn add storm-react-diagrams
```
**Via npm:**

View File

@@ -26,8 +26,8 @@ tell the mouse pointer to click and drag on various elements while making assert
We use Jest for the assertions and the interactivity is handled by puppeteer. Due to the laborious nature
of writing e2e tests, there is a helper method that is provided in each test that makes interacting
with the diagrams a lot easier. Using this helper, you cna easily tell the mouse to drag links between nodes,
with the diagrams a lot easier. Using this helper, you can easily tell the mouse to drag links between nodes,
select them and also easily assert information about them. The important thing here, is that this helper
does not touch the model in any way, but is purely a helper for writing the tests themselves. Please
make use of this helper when writing tests, as it ensure that the tests are defensive in nature, and also
reduces the overhead of physically writing them.
reduces the overhead of physically writing them.

7
jest-puppeteer.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
launch: {
dumpio: true,
headless: process.env.CI === 'true',
},
browserContext: 'default',
}

View File

@@ -1,27 +1,20 @@
const path = require("path");
// jest.config.js
module.exports = {
verbose: true,
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"preset": "jest-puppeteer",
transform: {
".*test_loader.*": path.join(__dirname, "tests", "helpers", "storybook-loader.js" ),
"^.+\\.tsx?$": "ts-jest",
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper:{
"\\.(scss|css|png)$": path.join(__dirname,"tests","helpers","css-mock.js"),
moduleNameMapper: {
"\\.(scss|css|png)$": path.join(__dirname, "tests", "helpers", "css-mock.js"),
"storm-react-diagrams": path.join(__dirname, "src", "main")
},
roots:[
__dirname+'/tests'
],
testMatch: [
"**/*\.test\.tsx"
"**/*\.test\.ts"
]
};

View File

@@ -1,6 +1,6 @@
{
"name": "storm-react-diagrams",
"version": "5.1.1",
"name": "@projectstorm/react-diagrams",
"version": "5.3.0",
"repository": {
"type": "git",
"url": "https://github.com/projectstorm/react-diagrams.git"
@@ -17,7 +17,7 @@
"nodes"
],
"main": "./dist/main.js",
"typings": "./dist/@types/src/main",
"typings": "./dist/@types/main",
"author": "dylanvorster",
"scripts": {
"build:ts": "webpack",
@@ -28,61 +28,71 @@
"storybook:github": "storybook-to-ghpages",
"pretty": "prettier --use-tabs --write \"{src,demos,tests}/**/*.{ts,tsx}\" --print-width 120",
"lint": "tslint -p .",
"test:ci": "rm -rf ./dist && node ./tests/e2e/generate-e2e.js && jest --no-cache",
"test:ci": "rm -rf ./dist && node ./tests/e2e/generate-e2e.js && jest --runInBand --no-cache",
"test": "jest --no-cache",
"prepublishOnly": "yarn run build:ts:prod && yarn run build:sass:prod"
"prepublishOnly": "rm -rf ./dist && yarn run build:ts:prod && yarn run build:sass:prod"
},
"dependencies": {
"peerDependencies": {
"closest": "^0.0.1",
"lodash": "^4.17.5",
"lodash": "4.*",
"pathfinding": "^0.4.18",
"paths-js": "^0.4.7",
"react": "^16.2.0"
"paths-js": "^0.4.9",
"react": "16.*"
},
"devDependencies": {
"@storybook/addon-actions": "^3.3.15",
"@storybook/addon-options": "^3.3.15",
"@storybook/addon-storyshots": "^3.3.15",
"@storybook/addons": "^3.3.15",
"@storybook/react": "^3.3.15",
"@storybook/storybook-deployer": "^2.3.0",
"@types/jest": "^22.2.0",
"@types/lodash": "^4.14.104",
"@types/node": "^9.4.7",
"@babel/core": "^7.5.5",
"@storybook/addon-actions": "^5.1.9",
"@storybook/addon-options": "^5.1.9",
"@storybook/addon-storyshots": "^5.1.9",
"@storybook/addons": "^5.1.9",
"@storybook/react": "^5.1.9",
"@storybook/storybook-deployer": "^2.8.1",
"@storybook/theming": "^5.1.9",
"@types/jest": "^24.0.15",
"@types/jest-environment-puppeteer": "^4.0.0",
"@types/lodash": "^4.14.136",
"@types/node": "^12.6.8",
"@types/promise": "^7.1.30",
"@types/puppeteer": "^1.1.0",
"@types/react": "^16.0.40",
"awesome-typescript-loader": "^4.0.1",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.1.4",
"css-loader": "^0.28.10",
"dagre": "^0.8.2",
"enzyme": "^3.3.0",
"file-loader": "^1.1.11",
"glob": "^7.1.2",
"jest": "^22.4.2",
"jest-cli": "^22.4.2",
"json-beautify": "^1.0.1",
"node-sass": "^4.7.2",
"prettier": "^1.11.1",
"puppeteer": "^1.1.1",
"raf": "^3.4.0",
"raw-loader": "^0.5.1",
"react-dom": "^16.2.0",
"react-syntax-highlighter": "^7.0.2",
"react-test-renderer": "^16.2.0",
"sass-loader": "^6.0.7",
"source-map-loader": "^0.2.3",
"storybook-host": "^4.1.5",
"storybook-readme": "^3.2.1",
"style-loader": "^0.20.3",
"ts-jest": "^22.4.1",
"tslint": "^5.9.1",
"ts-loader": "^4.1.0",
"typescript": "^2.7.2",
"uglifyjs-webpack-plugin": "^1.2.3",
"val-loader": "^1.1.0",
"webpack": "^4.1.1",
"webpack-cli": "^2.0.11"
"@types/puppeteer": "^1.12.4",
"@types/react": "^16.8.23",
"babel-jest": "^24.8.0",
"babel-loader": "^8.0.6",
"closest": "^0.0.1",
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.2.0",
"css-loader": "^3.1.0",
"dagre": "^0.8.4",
"enzyme": "^3.10.0",
"file-loader": "^4.1.0",
"glob": "^7.1.4",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
"jest-puppeteer": "^4.3.0",
"json-beautify": "^1.1.0",
"lodash": "4.*",
"node-sass": "^4.12.0",
"pathfinding": "^0.4.18",
"paths-js": "^0.4.9",
"prettier": "^1.18.2",
"puppeteer": "^1.18.1",
"raf": "^3.4.1",
"raw-loader": "^3.1.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-syntax-highlighter": "^11.0.2",
"react-test-renderer": "^16.8.6",
"sass-loader": "^7.1.0",
"source-map-loader": "^0.2.4",
"storybook-host": "^5.1.0",
"storybook-readme": "^5.0.5",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.3.0",
"ts-jest": "^24.0.2",
"ts-loader": "^6.0.4",
"typescript": "^3.5.3",
"val-loader": "^1.1.1",
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",
"webpack-node-externals": "^1.7.2"
}
}

3
postcss.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
plugins: [require('autoprefixer')]
};

115
src/BaseEntity.ts Normal file
View File

@@ -0,0 +1,115 @@
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,26 +1,365 @@
import { BaseEntity, BaseListener } from "./BaseEntity";
import { DiagramModel } from "./models/DiagramModel";
import { CanvasEngine } from "@projectstorm/react-canvas";
import { DefaultLabelFactory, DefaultLinkFactory, DefaultNodeFactory, DefaultPortFactory } from "storm-react-diagrams";
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;
export class DiagramEngine extends CanvasEngine<DiagramModel> {
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;
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 = {};
this.smartRouting = false;
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;
}
}
}
installDefaults() {
super.installDefaults();
this.registerElementFactory(new DefaultLabelFactory());
this.registerElementFactory(new DefaultLinkFactory());
this.registerElementFactory(new DefaultNodeFactory());
this.registerElementFactory(new DefaultPortFactory());
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
};
}
getMaxNumberPointsPerLink(): number {
@@ -32,10 +371,213 @@ export class DiagramEngine extends CanvasEngine<DiagramModel> {
}
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,11 +3,29 @@ 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 extends TK {
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);
});
}
/**
* Finds the closest element as a polyfill
*
@@ -27,8 +45,18 @@ export class Toolkit extends TK {
public static generateCurvePath(firstPoint: PointModel, lastPoint: PointModel, curvy: number = 0): string {
var isHorizontal = Math.abs(firstPoint.x - lastPoint.x) > Math.abs(firstPoint.y - lastPoint.y);
var curvyX = isHorizontal ? curvy : 0;
var curvyY = isHorizontal ? 0 : curvy;
var xOrY = isHorizontal ? "x" : "y";
// make sure that smoothening works
// without disrupting the line direction
let curvyness = curvy;
if (firstPoint[xOrY] > firstPoint[xOrY]) {
curvyness = -curvy;
}
var curvyX = isHorizontal ? curvyness : 0;
var curvyY = isHorizontal ? 0 : curvyness;
return `M${firstPoint.x},${firstPoint.y} C ${firstPoint.x + curvyX},${firstPoint.y + curvyY}
${lastPoint.x - curvyX},${lastPoint.y - curvyY} ${lastPoint.x},${lastPoint.y}`;

11
src/actions/BaseAction.ts Normal file
View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,36 @@
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,19 +1,22 @@
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";
export class DefaultLabelFactory extends AbstractElementFactory<DefaultLabelModel> {
/**
* @author Dylan Vorster
*/
export class DefaultLabelFactory extends AbstractLabelFactory<DefaultLabelModel> {
constructor() {
super("default");
}
generateWidget(engine: DiagramEngine, model: DefaultLabelModel): JSX.Element {
return <DefaultLabelWidget model={model} />;
generateReactWidget(diagramEngine: DiagramEngine, label: DefaultLabelModel): JSX.Element {
return <DefaultLabelWidget model={label} />;
}
generateModel(): DefaultLabelModel {
getNewInstance(initialConfig?: any): DefaultLabelModel {
return new DefaultLabelModel();
}
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { LabelModel } from "../../models/LabelModel";
import * as _ from "lodash";
import { DeserializeEvent } from "@projectstorm/react-canvas";
import { DiagramEngine } from "../../DiagramEngine";
export class DefaultLabelModel extends LabelModel {
protected label: string;
label: string;
constructor() {
super("default");
@@ -14,9 +14,9 @@ export class DefaultLabelModel extends LabelModel {
this.label = label;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.label = event.data.label;
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.label = ob.label;
}
serialize() {

View File

@@ -1,8 +1,13 @@
/**
* @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;
@@ -11,9 +16,9 @@ export interface DefaultLinkModelListener extends LinkModelListener {
}
export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
protected width: number;
protected color: string;
protected curvyness: number;
width: number;
color: string;
curvyness: number;
constructor(type: string = "default") {
super(type);
@@ -30,11 +35,11 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
});
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.color = event.data.color;
this.width = event.data.width;
this.curvyness = event.data.curvyness;
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.color = ob.color;
this.width = ob.width;
this.curvyness = ob.curvyness;
}
addLabel(label: LabelModel | string) {
@@ -48,7 +53,7 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
setWidth(width: number) {
this.width = width;
this.iterateListeners("width changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
this.iterateListeners((listener: DefaultLinkModelListener, event: BaseEvent) => {
if (listener.widthChanged) {
listener.widthChanged({ ...event, width: width });
}
@@ -57,22 +62,10 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
setColor(color: string) {
this.color = color;
this.iterateListeners("color changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
this.iterateListeners((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,12 +1,17 @@
import * as _ from "lodash";
import { DefaultPortModel } from "./DefaultPortModel";
import { NodeModel } from "../../models/NodeModel";
import { Toolkit } from "../../Toolkit";
import { DeserializeEvent } from "@projectstorm/react-canvas";
import * as _ from "lodash";
export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
protected name: string;
protected color: string;
import { NodeModel, NodeModelListener } from "../../models/NodeModel";
import { Toolkit } from "../../Toolkit";
import { DiagramEngine } from "../../DiagramEngine";
/**
* @author Dylan Vorster
*/
export class DefaultNodeModel extends NodeModel<NodeModelListener> {
name: string;
color: string;
ports: { [s: string]: DefaultPortModel };
constructor(name: string = "Untitled", color: string = "rgb(0,192,255)") {
super("default");
@@ -22,10 +27,10 @@ export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
return this.addPort(new DefaultPortModel(false, Toolkit.UID(), label));
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.name = event.data.name;
this.color = event.data.color;
deSerialize(object, engine: DiagramEngine) {
super.deSerialize(object, engine);
this.name = object.name;
this.color = object.color;
}
serialize() {
@@ -36,13 +41,13 @@ export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
}
getInPorts(): DefaultPortModel[] {
return _.filter(this.getPorts(), portModel => {
return _.filter(this.ports, portModel => {
return portModel.in;
});
}
getOutPorts(): DefaultPortModel[] {
return _.filter(this.getPorts(), portModel => {
return _.filter(this.ports, 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) {
super(name, "default");
constructor(isInput: boolean, name: string, label: string = null, id?: string) {
super(name, "default", id);
this.in = isInput;
this.label = label || name;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.in = event.data.in;
this.label = event.data.label;
deSerialize(object, engine: DiagramEngine) {
super.deSerialize(object, engine);
this.in = object.in;
this.label = object.label;
}
serialize() {

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { DefaultLabelModel } from "../models/DefaultLabelModel";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
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 "@projectstorm/react-canvas";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
export interface DefaultLinkProps extends BaseWidgetProps {
color?: string;
@@ -70,7 +70,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
}
}
addPointToLink(event: MouseEvent, index: number) {
addPointToLink = (event: MouseEvent, index: number): void => {
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].getPoint().x;
let y = this.props.link.points[pointIndex].getPoint().y;
let x = this.props.link.points[pointIndex].x;
let y = this.props.link.points[pointIndex].y;
return (
<g key={"point-" + this.props.link.points[pointIndex].getID()}>
<g key={"point-" + this.props.link.points[pointIndex].id}>
<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].getID()}
data-linkid={this.props.link.getID()}
data-id={this.props.link.points[pointIndex].id}
data-linkid={this.props.link.id}
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.getID()}
key={label.id}
className={this.bem("__label")}
width={canvas.offsetWidth}
height={canvas.offsetHeight}
>
<div ref={ref => (this.refLabels[label.getID()] = ref)}>
<div ref={ref => (this.refLabels[label.id] = ref)}>
{this.props.diagramEngine
.getFactoryForElement(label)
.generateWidget(this.props.diagramEngine, label)}
.getFactoryForLabel(label)
.generateReactWidget(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.getFactoryForElement(this.props.link) as DefaultLinkFactory).generateLinkSegment(
(props.diagramEngine.getFactoryForLink(this.props.link) as DefaultLinkFactory).generateLinkSegment(
this.props.link,
this,
this.state.selected || this.props.link.isSelected(),
@@ -267,10 +267,7 @@ 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).getPoint(),
_.last(points).getPoint()
);
const directPathCoords = this.pathFinding.calculateDirectPath(_.first(points), _.last(points));
const routingMatrix = diagramEngine.getRoutingMatrix();
// now we need to extract, from the routing matrix, the very first walkable points
@@ -308,9 +305,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
// See @link{#isSmartRoutingApplicable()}.
if (paths.length === 0) {
if (points.length === 2) {
var isHorizontal =
Math.abs(points[0].getPoint().x - points[1].getPoint().x) >
Math.abs(points[0].getPoint().x - points[1].getPoint().y);
var isHorizontal = Math.abs(points[0].x - points[1].x) > Math.abs(points[0].y - points[1].y);
var xOrY = isHorizontal ? "x" : "y";
//draw the smoothing
@@ -323,16 +318,9 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
var pointLeft = points[0];
var pointRight = points[1];
//some defensive programming to make sure the smoothing is
//always in the right direction
if (pointLeft[xOrY] > pointRight[xOrY]) {
pointLeft = points[1];
pointRight = points[0];
}
paths.push(
this.generateLink(
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.getCurvyness()),
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.curvyness),
{
onMouseDown: event => {
this.addPointToLink(event, 1);
@@ -353,7 +341,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
this.generateLink(
Toolkit.generateLinePath(points[j], points[j + 1]),
{
"data-linkid": this.props.link.getID(),
"data-linkid": this.props.link.id,
"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 "@projectstorm/react-canvas";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
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 "@projectstorm/react-canvas";
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
export interface DefaultPortLabelProps extends BaseWidgetProps {
model: DefaultPortModel;

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,5 @@
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,6 +3,7 @@
*/
export * from "./Toolkit";
export * from "./BaseEntity";
export * from "./DiagramEngine";
export * from "./defaults/models/DefaultNodeModel";
@@ -20,8 +21,21 @@ 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";
@@ -29,5 +43,11 @@ 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";

86
src/models/BaseModel.ts Normal file
View File

@@ -0,0 +1,86 @@
import { BaseEntity, BaseListener } from "../BaseEntity";
import * as _ from "lodash";
import { BaseEvent } from "../BaseEntity";
import { DiagramEngine } from "../DiagramEngine";
import { PointModel } from "../models/PointModel";
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(): Array<BaseModel<any, T> | PointModel> {
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,48 +1,240 @@
import { BaseListener, BaseEntity, BaseEvent, BaseEntityType } from "../BaseEntity";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "./LinkModel";
import { NodeModel } from "./NodeModel";
import { BaseModel, BaseEvent, CanvasModel, CanvasModelListener, CanvasLayerModel } from "@projectstorm/react-canvas";
export interface DiagramListener extends CanvasModelListener {
import { PortModel } from "./PortModel";
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PointModel } from "./PointModel";
/**
* @author Dylan Vorster
*
*/
export interface DiagramListener extends BaseListener {
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 CanvasModel<DiagramListener> {
linksLayer: CanvasLayerModel<LinkModel>;
nodesLayer: CanvasLayerModel<NodeModel>;
/**
*
*/
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;
constructor() {
super();
this.linksLayer = new CanvasLayerModel();
this.nodesLayer = new CanvasLayerModel();
this.links = {};
this.nodes = {};
this.linksLayer.setSVG(true);
this.linksLayer.setTransformable(true);
this.offsetX = 0;
this.offsetY = 0;
this.zoom = 100;
this.rendered = false;
this.gridSize = 0;
}
this.nodesLayer.setSVG(false);
this.nodesLayer.setTransformable(true);
setGridSize(size: number = 0) {
this.gridSize = size;
this.iterateListeners((listener, event) => {
if (listener.gridUpdated) {
listener.gridUpdated({ ...event, size: size });
}
});
}
this.addLayer(this.linksLayer);
this.addLayer(this.nodesLayer);
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;
}
getNode(node: string | NodeModel): NodeModel | null {
if (node instanceof NodeModel) {
return node;
}
return this.nodesLayer.getEntity[node] || null;
if (!this.nodes[node]) {
return null;
}
return this.nodes[node];
}
getLink(link: string | LinkModel): LinkModel | null {
if (link instanceof LinkModel) {
return link;
}
return this.linksLayer.getEntity(link) || null;
if (!this.links[link]) {
return null;
}
return this.links[link];
}
addAll(...models: BaseModel[]): BaseModel[] {
@@ -57,8 +249,13 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
}
addLink(link: LinkModel): LinkModel {
this.linksLayer.addEntity(link);
this.iterateListeners("link added", (listener, event) => {
link.addListener({
entityRemoved: () => {
this.removeLink(link);
}
});
this.links[link.getID()] = link;
this.iterateListeners((listener, event) => {
if (listener.linksUpdated) {
listener.linksUpdated({ ...event, link: link, isCreated: true });
}
@@ -67,8 +264,13 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
}
addNode(node: NodeModel): NodeModel {
this.nodesLayer.addEntity(node);
this.iterateListeners("node added", (listener, event) => {
node.addListener({
entityRemoved: () => {
this.removeNode(node);
}
});
this.nodes[node.getID()] = node;
this.iterateListeners((listener, event) => {
if (listener.nodesUpdated) {
listener.nodesUpdated({ ...event, node: node, isCreated: true });
}
@@ -77,8 +279,9 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
}
removeLink(link: LinkModel | string) {
this.linksLayer.removeEntity(link);
this.iterateListeners("link removed", (listener, event) => {
link = this.getLink(link);
delete this.links[link.getID()];
this.iterateListeners((listener, event) => {
if (listener.linksUpdated) {
listener.linksUpdated({ ...event, link: link as LinkModel, isCreated: false });
}
@@ -86,8 +289,9 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
}
removeNode(node: NodeModel | string) {
this.nodesLayer.removeEntity(node);
this.iterateListeners("node removed", (listener, event) => {
node = this.getNode(node);
delete this.nodes[node.getID()];
this.iterateListeners((listener, event) => {
if (listener.nodesUpdated) {
listener.nodesUpdated({ ...event, node: node as NodeModel, isCreated: false });
}
@@ -95,10 +299,10 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
}
getLinks(): { [s: string]: LinkModel } {
return this.linksLayer.getEntities();
return this.links;
}
getNodes(): { [s: string]: NodeModel } {
return this.nodesLayer.getEntities();
return this.nodes;
}
}

View File

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

View File

@@ -1,71 +1,97 @@
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 {
BaseEvent,
GraphModel,
GraphModelOrdered,
CanvasElementModel,
CanvasElementModelListener,
Rectangle,
DeserializeEvent
} from "@projectstorm/react-canvas";
import { DiagramEngine } from "../DiagramEngine";
import { DiagramModel } from "./DiagramModel";
export interface LinkModelListener<T extends LinkModel = any> extends CanvasElementModelListener<T> {
sourcePortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
export interface LinkModelListener extends BaseModelListener {
sourcePortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
}
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>;
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends BaseModel<DiagramModel, T> {
sourcePort: PortModel | null;
targetPort: PortModel | null;
labels: LabelModel[];
points: PointModel[];
extras: any;
constructor(linkType: string = "default") {
super(linkType);
this.points = new GraphModelOrdered();
this.labels = new GraphModel();
this.points.setParentDelegate(this);
this.labels.setParentDelegate(this);
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 = {};
this.sourcePort = null;
this.targetPort = null;
this.labels = [];
}
setDimensions(dimensions: Rectangle) {
throw new Error("Method not implemented.");
}
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;
});
getDimensions(): 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);
});
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.target) {
this.setTargetPort(
this.getParent()
.getNode(ob.target)
.getPortFromID(ob.targetPort)
);
}
if (event.data.source) {
this.setSourcePort(event.cache[event.data.sourcePort] as PortModel);
if (ob.source) {
this.setSourcePort(
this.getParent()
.getNode(ob.source)
.getPortFromID(ob.sourcePort)
);
}
}
serialize() {
return _.merge(super.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()
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();
})
});
}
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);
@@ -73,19 +99,25 @@ 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.count() - 1;
return index === this.points.length - 1;
}
getPointIndex(point: PointModel) {
return _.values(this.points.getEntities()).indexOf(point);
return this.points.indexOf(point);
}
getPointModel(id: string): PointModel | null {
return this.points.getEntities()[id];
for (var i = 0; i < this.points.length; i++) {
if (this.points[i].id === id) {
return this.points[i];
}
}
return null;
}
getPortForPoint(point: PointModel): PortModel {
@@ -109,11 +141,11 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
}
getFirstPoint(): PointModel {
return _.values(this.points.getEntities())[0];
return this.points[0];
}
getLastPoint(): PointModel {
return _.values(this.points.getEntities())[this.points.count() - 1];
return this.points[this.points.length - 1];
}
setSourcePort(port: PortModel) {
@@ -124,7 +156,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
this.sourcePort.removeLink(this);
}
this.sourcePort = port;
this.iterateListeners("source port changed", (listener: T, event) => {
this.iterateListeners((listener: LinkModelListener, event) => {
if (listener.sourcePortChanged) {
listener.sourcePortChanged({ ...event, port: port });
}
@@ -147,7 +179,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
this.targetPort.removeLink(this);
}
this.targetPort = port;
this.iterateListeners("target port chnaged", (listener: T, event) => {
this.iterateListeners((listener: LinkModelListener, event) => {
if (listener.targetPortChanged) {
listener.targetPortChanged({ ...event, port: port });
}
@@ -159,34 +191,46 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
}
addLabel(label: LabelModel) {
this.labels.addEntity(label);
label.setParent(this);
this.labels.push(label);
}
getPoints(): PointModel[] {
return this.points.getArray();
return this.points;
}
setPoints(points: PointModel[]) {
_.forEach(points, point => {
point.setLink(this);
point.setParent(this);
});
this.points.addEntities(points);
this.points = points;
}
removePoint(pointModel: PointModel) {
this.points.removeEntity(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);
}
}
addPoint<P extends PointModel>(pointModel: P, index = 1): P {
pointModel.setLink(this);
this.points.addEntity(pointModel, index);
pointModel.setParent(this);
this.points.splice(index, 0, pointModel);
return pointModel;
}
generatePoint(x: number, y: number): PointModel {
let point = new PointModel(this);
point.getPoint().x = x;
point.getPoint().y = y;
return point;
generatePoint(x: number = 0, y: number = 0): PointModel {
return new PointModel(this, { x: x, y: y });
}
}

View File

@@ -1,67 +1,131 @@
import { BaseEvent } from "../BaseEntity";
import { BaseModel, BaseModelListener } from "./BaseModel";
import { LinkModel, LinkModelListener } from "./LinkModel";
import { PortModel } from "./PortModel";
import * as _ from "lodash";
import { Rectangle, CanvasElementModel, GraphModel, DeserializeEvent } from "@projectstorm/react-canvas";
import { DiagramEngine } from "../DiagramEngine";
import { DiagramModel } from "./DiagramModel";
import { PointModel } from "./PointModel";
export class NodeModel<T extends PortModel = PortModel> extends CanvasElementModel {
protected dimensions: Rectangle;
protected ports: GraphModel<T, null>;
export interface NodeModelListener extends BaseModelListener {
positionChanged?(event: BaseEvent<NodeModel>): void;
}
constructor(nodeType: string = "default") {
super(nodeType);
this.dimensions = new Rectangle(0, 0, 0, 0);
this.ports = new GraphModel("ports");
export class NodeModel<T extends NodeModelListener = NodeModelListener> extends BaseModel<DiagramModel, T> {
x: number;
y: number;
extras: any;
ports: { [s: string]: PortModel };
// 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 = {};
}
setDimensions(dimensions: Rectangle) {
this.dimensions = dimensions;
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;
}
getDimensions(): Rectangle {
return this.dimensions;
positionChanged() {
this.iterateListeners(
(listener: NodeModelListener, event) => listener.positionChanged && listener.positionChanged(event)
);
}
setPosition(x:number, y:number){
this.dimensions.updateDimensions(x,y, this.dimensions.getWidth(), this.dimensions.getHeight());
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;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.dimensions.deserialize(event.data.dimensions);
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.x = ob.x;
this.y = ob.y;
this.extras = ob.extras;
//deserialize ports
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));
_.forEach(ob.ports, (port: any) => {
let portOb = engine.getPortFactory(port.type).getNewInstance();
portOb.deSerialize(port, engine);
this.addPort(portOb);
});
}
serialize() {
return _.merge(super.serialize(), {
dimensions: this.dimensions.serialize(),
ports: this.ports.serialize()
x: this.x,
y: this.y,
extras: this.extras,
ports: _.map(this.ports, port => {
return port.serialize();
})
});
}
getPortFromID(id): T | null {
for (let i in this.ports) {
if (this.ports[i].getID() === id) {
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) {
return this.ports[i];
}
}
return null;
}
getPort(name: string): T | null {
return this.ports.getEntities()[name];
getPort(name: string): PortModel | null {
return this.ports[name];
}
getPorts(): { [s: string]: T } {
return this.ports.getEntities();
getPorts(): { [s: string]: PortModel } {
return this.ports;
}
removePort(port: T) {
removePort(port: PortModel) {
//clear the parent node reference
if (this.ports[port.name]) {
this.ports[port.name].setParent(null);
@@ -69,8 +133,14 @@ export class NodeModel<T extends PortModel = PortModel> extends CanvasElementMod
}
}
addPort(port: T): T {
this.ports.addEntity(port);
addPort<T extends PortModel>(port: T): T {
port.setParent(this);
this.ports[port.name] = port;
return port;
}
updateDimensions({ width, height }: { width: number; height: number }) {
this.width = width;
this.height = height;
}
}

View File

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

View File

@@ -1,37 +1,49 @@
import { BaseModel, BaseListener, DeserializeEvent } from "@projectstorm/react-canvas";
import { BaseModel, BaseModelListener } from "./BaseModel";
import { NodeModel } from "./NodeModel";
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
export class PortModel extends BaseModel<NodeModel, BaseListener> {
export class PortModel extends BaseModel<NodeModel, BaseModelListener> {
name: string;
links: { [id: string]: LinkModel };
maximumLinks: number;
constructor(name: string, type?: string, maximumLinks?: number) {
super(type);
// 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);
this.name = name;
this.links = {};
this.maximumLinks = maximumLinks;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.name = event.data.name;
this.maximumLinks = event.data.maximumLinks;
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.name = ob.name;
this.maximumLinks = ob.maximumLinks;
}
serialize() {
return _.merge(super.serialize(), {
name: this.name,
parentNode: this.parent.getID(),
parentNode: this.parent.id,
links: _.map(this.links, link => {
return link.getID();
return link.id;
}),
maximumLinks: this.maximumLinks
});
}
doClone(lookupTable = {}, clone) {
clone.links = {};
clone.parentNode = this.getParent().clone(lookupTable);
}
getNode(): NodeModel {
return this.getParent();
}
@@ -72,7 +84,18 @@ export class PortModel extends BaseModel<NodeModel, BaseListener> {
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

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

133
src/routing/PathFinding.ts Normal file
View File

@@ -0,0 +1,133 @@
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

@@ -0,0 +1,11 @@
.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

@@ -0,0 +1,11 @@
.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,4 +1,6 @@
@import "DiagramWidget";
@import "LinkLayerWidget";
@import "NodeLayerWidget";
@import "NodeWidget";
@import "PortWidget";
@@ -6,4 +8,4 @@
@import "defaults/DefaultNodeWidget";
@import "defaults/DefaultPortWidget";
@import "defaults/DefaultLabelWidget";
@import "defaults/DefaultLinkWidget";
@import "defaults/DefaultLinkWidget";

View File

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,557 @@
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);
if (model.model instanceof NodeModel) {
model.model.positionChanged();
// update port coordinates as well
_.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 necessarily 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);
_.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)) {
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()];
}
});
//check for / remove any loose links in any models which have been moved
if (!this.props.allowLooseLinks && this.state.wasMoved) {
_.forEach(this.state.action.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
let selectedPoint: PointModel = model.model;
let link: LinkModel = selectedPoint.getLink();
if (link.getSourcePort() === null || link.getTargetPort() === null) {
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;
}
let link: LinkModel = model.model.getLink();
let sourcePort: PortModel = link.getSourcePort();
let 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 => {
if (event.nativeEvent.which === 3) return;
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

@@ -0,0 +1,30 @@
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,7 +1,8 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import { NodeModel } from "../models/NodeModel";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
import { Toolkit } from "../Toolkit";
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
export interface NodeProps extends BaseWidgetProps {
node: NodeModel;
@@ -11,12 +12,19 @@ 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") : "");
}
@@ -25,10 +33,10 @@ export class NodeWidget extends BaseWidget<NodeProps, NodeState> {
return (
<div
{...this.getProps()}
data-nodeid={this.props.node.getID()}
data-nodeid={this.props.node.id}
style={{
top: this.props.node.getDimensions().getTopLeft().y,
left: this.props.node.getDimensions().getTopLeft().x
top: this.props.node.y,
left: this.props.node.x
}}
>
{this.props.children}

View File

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

View File

@@ -0,0 +1,92 @@
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

@@ -0,0 +1,64 @@
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

@@ -46,7 +46,7 @@ export class E2EPort extends E2EElement {
// click on this port
this.page.mouse.move(bounds.x, bounds.y);
this.page.mouse.down();
//
let bounds2 = await port.element.boundingBox();
// drag to other port
@@ -72,10 +72,13 @@ export class E2EPort extends E2EElement {
this.page.mouse.move(x, y);
this.page.mouse.up();
const link = _.difference(_.flatMap((await this.parent.model()).ports, "links"), currentLinks)[0];
if(!link){
return null;
}
// get the parent to get the link
return await this.helper.link(
_.difference(_.flatMap((await this.parent.model()).ports, "links"), currentLinks)[0]
);
return await this.helper.link(link);
}
}
@@ -98,7 +101,11 @@ export class E2ELink extends E2EElement {
async select(): Promise<any> {
const point = await this.page.evaluate(id => {
const path = document.querySelector(`path[data-linkid="${id}"]`) as SVGPathElement;
return path.getPointAtLength(path.getTotalLength() / 2);
const point =path.getPointAtLength(path.getTotalLength() / 2);
return {
x: point.x,
y: point.y
}
}, this.id);
await this.page.keyboard.down("Shift");
await this.page.mouse.move(point.x, point.y);
@@ -115,11 +122,19 @@ export class E2EHelper {
}
async link(id): Promise<E2ELink> {
if(!id){
throw "Link ID must be valid"
}
let selector = await this.page.waitForSelector(`path[data-linkid="${id}"]`);
return new E2ELink(this, this.page, selector, id);
}
async node(id): Promise<E2ENode> {
if(!id){
if(!id){
throw "Node ID must be valid"
}
}
let selector = await this.page.waitForSelector(`div[data-nodeid="${id}"]`);
return new E2ENode(this, this.page, selector, id);
}

View File

@@ -37,8 +37,6 @@ glob.glob(__dirname + "/../../demos/demo-*/index.tsx", {}, (err, files) => {
},
};
console.log(config);
webpack(config, (err, stats) => {
if (err || stats.hasErrors()) {
// Handle errors here

View File

@@ -1,60 +1,36 @@
import "jest";
import * as puppeteer from "puppeteer";
import { E2EHelper } from "./E2EHelper";
import {E2EHelper} from "./E2EHelper";
var browser;
describe("simple flow test", () => {
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();
beforeEach(async () => {
await page.goto(`file://${__dirname}/../../dist/e2e/demo-simple-flow/index.html`);
});
}
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) => {
it("drag link to port adds a link", async () => {
// create a new link
let node1 = await helper.node("6");
let helper = new E2EHelper(page);
let node1 = await helper.node("17");
let node2 = await helper.node("9");
let port1 = await node1.port("7");
let port1 = await node1.port("18");
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) => {
it("drag link to node does not add a link", async () => {
// create a new link
let node1 = await helper.node("6");
let helper = new E2EHelper(page);
let node1 = await helper.node("17");
let node2 = await helper.node("9");
let port1 = await node1.port("7");
let port1 = await node1.port("18");
let node2Bounds = await node2.element.boundingBox();
let newlink = await port1.linkToPoint(node2Bounds.x, node2Bounds.y);
await expect(await newlink.exists()).toBeFalsy();
await expect(newlink).toBeNull();
});
});

View File

@@ -1,46 +1,21 @@
import "jest";
import * as puppeteer from "puppeteer";
import { E2EHelper } from "./E2EHelper";
import {E2EHelper} from "./E2EHelper";
var browser;
describe("simple test", () => {
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 () => {
await page.goto(`file://${__dirname}/../../dist/e2e/demo-simple/index.html`)
});
}
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 test", async () => {
itShould("demo-simple", "should delete a link and create a new one", async (page, helper) => {
it("should delete a link and create a new one", async () => {
// get the existing link
let helper = new E2EHelper(page);
let link = await helper.link("12");
await expect(await link.exists()).toBeTruthy();
// remove it
await link.select();
await page.keyboard.press("Del");
await page.keyboard.press("Delete");
await expect(await link.exists()).toBeFalsy();

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
import initStoryshots from "@storybook/addon-storyshots";
import "raf/polyfill";
initStoryshots({ configPath: __dirname });
initStoryshots();

View File

@@ -1,5 +1,4 @@
import * as React from "react";
import { storiesOf, addDecorator } from "@storybook/react";
import { Toolkit } from "../../src/Toolkit";
Toolkit.TESTING = true;

View File

@@ -4,8 +4,7 @@
"compilerOptions": {
"suppressExcessPropertyErrors": true,
"declaration": true,
"outDir": "@types",
"target": "es5",
"outDir": "dist/@types",
"strictNullChecks": false,
"sourceMap": true,
"skipLibCheck": true,
@@ -14,11 +13,16 @@
"paths": {
"storm-react-diagrams": ["src/main.ts"]
},
"target": "es6",
"module": "commonjs",
"lib": [
"dom",
"es2015"
"es6"
]
},
"include": [
"./src"
],
"exclude": [
"node_modules",
"**/*.spec.ts",

View File

@@ -1,31 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"member-access": false,
"comment-format": false,
"max-line-length": false,
"object-literal-sort-keys": false,
"quotemark": [true, "double", "jsx-double"],
"arrow-parens": false,
"indent": [true, "tabs", 2],
"semicolon": false,
"object-literal-key-quotes": [true, "as-needed"],
"no-var-keyword": false,
"jsdoc-format": false,
"prefer-const": false,
"interface-name": false,
"array-type": false,
"trailing-comma": false,
"one-line": false,
"object-literal-shorthand": false,
"no-string-literal": false,
"ordered-imports": false,
"prefer-for-of": false,
"no-empty-interface": false
},
"rulesDirectory": []
}

View File

@@ -1,22 +1,7 @@
const webpack = require("webpack");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
var path = require("path");
var plugins = [];
const production = process.env.NODE_ENV === "production";
const TerserPlugin = require('terser-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
//do we minify it all
if (production) {
console.log("creating production build");
plugins.push(
new webpack.DefinePlugin({
"process.env.NODE_ENV": '"production"'
})
);
}
/**
* @author Dylan Vorster
*/
module.exports =
//for building the umd distribution
{
@@ -27,27 +12,7 @@ module.exports =
libraryTarget: "umd",
library: "storm-react-diagrams"
},
externals: {
react: {
root: "React",
commonjs2: "react",
commonjs: "react",
amd: "react"
},
"react-dom": {
root: "ReactDOM",
commonjs2: "react-dom",
commonjs: "react-dom",
amd: "react-dom"
},
lodash: {
commonjs: "lodash",
commonjs2: "lodash",
amd: "_",
root: "_"
}
},
plugins: plugins,
externals: [nodeExternals()],
module: {
rules: [
{
@@ -67,16 +32,6 @@ module.exports =
devtool: production ? "source-map" : "cheap-module-source-map",
mode: production ? "production" : "development",
optimization: {
minimizer: [
// we specify a custom UglifyJsPlugin here to get source maps in production
new UglifyJsPlugin({
uglifyOptions: {
compress: false,
ecma: 5,
mangle: false
},
sourceMap: true
})
]
minimizer: [new TerserPlugin()],
}
};

10716
yarn.lock
View File

File diff suppressed because it is too large Load Diff