├── .editorconfig ├── .envrc ├── .gitignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .yarnrc ├── LICENSE ├── README.md ├── demos ├── .helpers │ └── demo.scss ├── demo-standard │ └── index.story.tsx └── index.tsx ├── images ├── 1.gif ├── 2.gif ├── 3.gif ├── 4.gif ├── 5.gif ├── 6.gif └── screenshot.jpg ├── package.json ├── src ├── AbstractElementFactory.ts ├── CanvasEngine.ts ├── CanvasLayerFactory.tsx ├── base-models │ ├── BaseModel.ts │ ├── GraphModel.ts │ └── GraphModelOrdered.ts ├── event-bus │ ├── Action.ts │ ├── Event.ts │ ├── EventBus.ts │ ├── InlineAction.ts │ ├── actions │ │ ├── DeselectModelsAction.ts │ │ ├── SelectCanvasAction.ts │ │ ├── SelectElementAction.ts │ │ └── ZoomCanvasAction.ts │ └── events │ │ ├── ModelEvent.ts │ │ ├── elements.ts │ │ ├── key.ts │ │ └── mouse.ts ├── geometry │ ├── Point.ts │ ├── Polygon.ts │ └── Rectangle.ts ├── history │ ├── HistoryBank.ts │ └── HistoryState.ts ├── main.ts ├── models-canvas │ ├── CanvasElementModel.ts │ ├── CanvasGroupModel.ts │ ├── CanvasLayerModel.ts │ └── CanvasModel.ts ├── primitives │ ├── ellipse │ │ ├── EllipseElementFactory.tsx │ │ ├── EllipseElementModel.tsx │ │ └── EllipseElementWidget.tsx │ ├── grid │ │ ├── GridElementFactory.tsx │ │ ├── GridElementModel.ts │ │ └── GridElementWidget.tsx │ ├── paper │ │ ├── PaperElementFactory.tsx │ │ ├── PaperElementModel.ts │ │ └── PaperElementWidget.tsx │ ├── rectangle │ │ ├── RectangleElementFactory.tsx │ │ ├── RectangleElementModel.ts │ │ └── RectangleElementWidget.tsx │ └── selection │ │ ├── SelectionElementFactory.tsx │ │ ├── SelectionElementModel.ts │ │ ├── SelectionElementWidget.tsx │ │ └── SelectionGroupWidget.tsx ├── sass │ ├── _AnchorWidget.scss │ ├── _CanvasLayerWidget.scss │ ├── _CanvasWidget.scss │ ├── _PaperElementWidget.scss │ ├── _SelectionGroupWidget.scss │ └── main.scss ├── state-machine │ ├── AbstractDisplacementState.ts │ ├── AbstractState.ts │ ├── AbstractStateMachineInput.ts │ ├── StateMachine.ts │ ├── input │ │ ├── KeyInput.ts │ │ ├── ModelAnchorInput.ts │ │ ├── ModelElementInput.ts │ │ ├── ModelRotateInput.ts │ │ └── MouseDownInput.ts │ └── states │ │ ├── DefaultState.ts │ │ ├── ResizeDimensionsState.ts │ │ ├── ResizeOriginDimensionState.ts │ │ ├── RotateElementsState.ts │ │ ├── SelectElementsState.ts │ │ ├── TranslateCanvasState.ts │ │ └── TranslateElementState.ts ├── tracking │ ├── DimensionTracker.ts │ ├── DimensionTrackerWidget.tsx │ └── VirtualDimensionTracker.ts └── widgets │ ├── AnchorWidget.tsx │ ├── CanvasLayerWidget.tsx │ └── CanvasWidget.tsx ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 4 4 | trim_trailing_whitespace = true 5 | 6 | # Some exceptions 7 | [{package.json}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | PATH_add ./node_modules/.bin 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | dist/main.js 3 | dist/main.js.map 4 | 5 | .out 6 | 7 | # Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node 8 | 9 | #!! ERROR: net is undefined. Use list command to see defined gitignore types !!# 10 | 11 | ### NetBeans ### 12 | nbproject/private/ 13 | build/ 14 | nbbuild/ 15 | nbdist/ 16 | nbactions.xml 17 | .nb-gradle/ 18 | 19 | 20 | ### SublimeText ### 21 | # cache files for sublime text 22 | *.tmlanguage.cache 23 | *.tmPreferences.cache 24 | *.stTheme.cache 25 | 26 | # workspace files are user-specific 27 | *.sublime-workspace 28 | 29 | # project files should be checked into the repository, unless a significant 30 | # proportion of contributors will probably not be using SublimeText 31 | # *.sublime-project 32 | 33 | # sftp configuration file 34 | sftp-config.json 35 | 36 | # Package control specific files 37 | Package Control.last-run 38 | Package Control.ca-list 39 | Package Control.ca-bundle 40 | Package Control.system-ca-bundle 41 | Package Control.cache/ 42 | Package Control.ca-certs/ 43 | bh_unicode_properties.cache 44 | 45 | # Sublime-github package stores a github token in this file 46 | # https://packagecontrol.io/packages/sublime-github 47 | GitHub.sublime-settings 48 | 49 | 50 | ### PhpStorm ### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 52 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 53 | 54 | # User-specific stuff: 55 | .idea/workspace.xml 56 | .idea/tasks.xml 57 | .idea/dictionaries 58 | .idea/vcs.xml 59 | .idea/jsLibraryMappings.xml 60 | 61 | # Sensitive or high-churn files: 62 | .idea/dataSources.ids 63 | .idea/dataSources.xml 64 | .idea/dataSources.local.xml 65 | .idea/sqlDataSources.xml 66 | .idea/dynamic.xml 67 | .idea/uiDesigner.xml 68 | 69 | # Gradle: 70 | .idea/gradle.xml 71 | .idea/libraries 72 | 73 | # Mongo Explorer plugin: 74 | .idea/mongoSettings.xml 75 | 76 | ## File-based project format: 77 | *.iws 78 | 79 | ## Plugin-specific files: 80 | 81 | # IntelliJ 82 | /out/ 83 | 84 | # mpeltonen/sbt-idea plugin 85 | .idea_modules/ 86 | 87 | # JIRA plugin 88 | atlassian-ide-plugin.xml 89 | 90 | # Crashlytics plugin (for Android Studio and IntelliJ) 91 | com_crashlytics_export_strings.xml 92 | crashlytics.properties 93 | crashlytics-build.properties 94 | fabric.properties 95 | 96 | ### PhpStorm Patch ### 97 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 98 | 99 | # *.iml 100 | # modules.xml 101 | 102 | 103 | ### Windows ### 104 | # Windows image file caches 105 | Thumbs.db 106 | ehthumbs.db 107 | 108 | # Folder config file 109 | Desktop.ini 110 | 111 | # Recycle Bin used on file shares 112 | $RECYCLE.BIN/ 113 | 114 | # Windows Installer files 115 | *.cab 116 | *.msi 117 | *.msm 118 | *.msp 119 | 120 | # Windows shortcuts 121 | *.lnk 122 | 123 | 124 | ### OSX ### 125 | *.DS_Store 126 | .AppleDouble 127 | .LSOverride 128 | 129 | # Icon must end with two \r 130 | Icon 131 | 132 | 133 | # Thumbnails 134 | ._* 135 | 136 | # Files that might appear in the root of a volume 137 | .DocumentRevisions-V100 138 | .fseventsd 139 | .Spotlight-V100 140 | .TemporaryItems 141 | .Trashes 142 | .VolumeIcon.icns 143 | .com.apple.timemachine.donotpresent 144 | 145 | # Directories potentially created on remote AFP share 146 | .AppleDB 147 | .AppleDesktop 148 | Network Trash Folder 149 | Temporary Items 150 | .apdisk 151 | 152 | 153 | ### Node ### 154 | # Logs 155 | logs 156 | *.log 157 | npm-debug.log* 158 | 159 | # Runtime data 160 | pids 161 | *.pid 162 | *.seed 163 | 164 | # Directory for instrumented libs generated by jscoverage/JSCover 165 | lib-cov 166 | 167 | # Coverage directory used by tools like istanbul 168 | coverage 169 | 170 | # nyc test coverage 171 | .nyc_output 172 | 173 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 174 | .grunt 175 | 176 | # node-waf configuration 177 | .lock-wscript 178 | 179 | # Compiled binary addons (http://nodejs.org/api/addons.html) 180 | build/Release 181 | 182 | # Dependency directories 183 | node_modules 184 | jspm_packages 185 | 186 | # Optional npm cache directory 187 | .npm 188 | 189 | # Optional REPL history 190 | .node_repl_history 191 | .idea 192 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-options/register'; 3 | import '@storybook/addon-knobs/register'; 4 | import '@storybook/addon-storysource/register'; 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../demos/index.tsx'); 5 | require('../demos/demo-standard/index.story'); 6 | // You can require as many demos as you need. 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | module: { 4 | rules: [ 5 | { 6 | test: /\.story\.tsx?$/, 7 | loaders: [ 8 | { 9 | loader: require.resolve('@storybook/addon-storysource/loader'), 10 | options: { parser: 'typescript' } 11 | } 12 | ], 13 | enforce: 'pre', 14 | }, 15 | { 16 | test: /\.scss$/, 17 | loaders: ["style-loader", "css-loader", "sass-loader"], 18 | include: path.resolve(__dirname, '../') 19 | }, 20 | { 21 | test: /\.css/, 22 | loaders: ["style-loader", "css-loader"], 23 | include: path.resolve(__dirname, '../') 24 | }, 25 | { 26 | enforce: 'pre', 27 | test: /\.js$/, 28 | loader: "source-map-loader", 29 | exclude: [ 30 | /node_modules\// 31 | ] 32 | }, 33 | { 34 | test: /\.tsx?$/, 35 | loader: 'awesome-typescript-loader?declaration=false&transpileOnly=true', 36 | }, 37 | { 38 | test: /\.(woff|woff2|eot|ttf|otf|svg)$/, 39 | loader: "file-loader" 40 | }, 41 | ] 42 | }, 43 | resolve: { 44 | alias: { 45 | '@projectstorm/react-canvas': path.join(__dirname, "..", "src", "main") 46 | }, 47 | extensions: [".tsx", ".ts", ".js"] 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Storm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Note: this project was an experiment for me (Dylan Vorster) where I learnt all about desktop publishing systems (DTP). A lot of the concepts I implemented here, I moved over to react-diagrams such as layers etc.. I still plan to revisit a lot of these ideas there, and maybe port over some of the elements such as the infinite grid and page layout. Thanks for your interest in this project though, it meant a lot to me <3 , I never thought a throwaway project like this would end up with so many Github stars x_x 2 | 3 | 4 | # STORM React canvas (Archived) 5 | 6 | A brand new foundation for storm-react-diagrams 7 | 8 | ![](./images/screenshot.jpg) 9 | 10 | ## Features 11 | 12 | #### Core 13 | * virtual co-ordinate system 14 | * real co-ordinate system 15 | * matrix transformation, precomputed and optimized 16 | * forward dimensions and inferred dimensions 17 | * history push and pop (wip) 18 | * SVG and standard DOM support 19 | * pluggable event bus 20 | * pluggable state machine 21 | * model driven with serialization layers (imperative and declarative styles) 22 | * serialization and deserialization 23 | 24 | #### Canvas 25 | * translate / panning 26 | * zoom 27 | * de-select elements 28 | * fit to width 29 | * multiple ordered layers 30 | * ordered elements on layers 31 | 32 | #### Primitives 33 | * multiple infinite grids 34 | * point based vectors (wip) 35 | * ellipse 36 | * rectangles 37 | * pages 38 | 39 | ### Groups 40 | * group selection 41 | * group translate 42 | * group scaling from any anchor point 43 | * group rotation from origin (or any arbitrary point) 44 | * group transform with mirror modifiers (wip) 45 | 46 | 47 | | | | 48 | |---|---| 49 | | ![](./images/1.gif) | ![](./images/2.gif) | 50 | | ![](./images/3.gif) | ![](./images/4.gif) | 51 | | ![](./images/5.gif) | ![](./images/6.gif) | 52 | -------------------------------------------------------------------------------- /demos/.helpers/demo.scss: -------------------------------------------------------------------------------- 1 | html,body,#root{ 2 | height: 100%; 3 | } 4 | .demo-canvas{ 5 | height: 600px; 6 | width: 100%; 7 | } -------------------------------------------------------------------------------- /demos/demo-standard/index.story.tsx: -------------------------------------------------------------------------------- 1 | import { CanvasWidget } from "../../src/widgets/CanvasWidget"; 2 | import * as React from "react"; 3 | import { CanvasEngine } from "../../src/CanvasEngine"; 4 | import { CanvasModel } from "../../src/models-canvas/CanvasModel"; 5 | import { CanvasLayerModel } from "../../src/models-canvas/CanvasLayerModel"; 6 | import { RectangleElementModel } from "../../src/primitives/rectangle/RectangleElementModel"; 7 | 8 | import { storiesOf } from "@storybook/react"; 9 | import { button } from "@storybook/addon-knobs"; 10 | import { GridElementModel } from "../../src/primitives/grid/GridElementModel"; 11 | import { PaperElementModel } from "../../src/primitives/paper/PaperElementModel"; 12 | 13 | storiesOf("Simple Usage", module).add("Full example", () => { 14 | //setup canvas engine 15 | let engine = new CanvasEngine(); 16 | engine.enableDebugMode(true); 17 | engine.installDefaults(); 18 | 19 | let model = new CanvasModel(); 20 | model.setOffset(100, 100); 21 | model.setZoomLevel(1); 22 | engine.setModel(model); 23 | 24 | // grid layer 25 | let layer2 = new CanvasLayerModel(); 26 | layer2.setSVG(true); 27 | layer2.setTransformable(false); 28 | model.addLayer(layer2); 29 | 30 | let gridModel = new GridElementModel(); 31 | layer2.addModel(gridModel); 32 | 33 | let gridModel2 = new GridElementModel(); 34 | gridModel2.sizeX = 200; 35 | gridModel2.sizeY = 200; 36 | gridModel2.color = "cyan"; 37 | gridModel2.thickness = 2; 38 | layer2.addModel(gridModel2); 39 | 40 | // paper layer 41 | let paperLayer = new CanvasLayerModel(); 42 | paperLayer.setSVG(false); 43 | paperLayer.setTransformable(true); 44 | let paper = new PaperElementModel(); 45 | paperLayer.addModel(paper); 46 | model.addLayer(paperLayer); 47 | 48 | // add layer 49 | let layer = new CanvasLayerModel(); 50 | layer.setSVG(true); 51 | layer.setTransformable(true); 52 | model.addLayer(layer); 53 | 54 | // squares 55 | let squareModel = new RectangleElementModel(); 56 | squareModel.dimensions.updateDimensions(-100, -100, 100, 100); 57 | 58 | let squareModel2 = new RectangleElementModel(); 59 | squareModel2.dimensions.updateDimensions(300, 300, 50, 70); 60 | 61 | let squareModel3 = new RectangleElementModel(); 62 | squareModel3.dimensions.updateDimensions(420, 420, 50, 70); 63 | 64 | layer.addModels([squareModel, squareModel2, squareModel3]); 65 | 66 | button("Fit Width", () => { 67 | engine.getCanvasWidget().zoomToFit(15); 68 | }); 69 | 70 | button("Undo", () => { 71 | engine.getHistoryBank().goBackward(); 72 | }); 73 | 74 | button("Redo", () => { 75 | engine.getHistoryBank().goForward(); 76 | }); 77 | 78 | return ; 79 | }); 80 | -------------------------------------------------------------------------------- /demos/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf, addDecorator } from "@storybook/react"; 3 | import { setOptions } from "@storybook/addon-options"; 4 | import { withKnobs, text, boolean, number } from "@storybook/addon-knobs/react"; 5 | import { configureViewport } from '@storybook/addon-viewport'; 6 | //include the SCSS for the demo 7 | import "./.helpers/demo.scss"; 8 | import "../src/sass/main.scss"; 9 | 10 | addDecorator(withKnobs); 11 | 12 | setOptions({ 13 | name: "STORM React Canvas", 14 | url: "https://github.com/projectstorm/react-canvas", 15 | addonPanelInRight: true 16 | }); 17 | -------------------------------------------------------------------------------- /images/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/1.gif -------------------------------------------------------------------------------- /images/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/2.gif -------------------------------------------------------------------------------- /images/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/3.gif -------------------------------------------------------------------------------- /images/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/4.gif -------------------------------------------------------------------------------- /images/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/5.gif -------------------------------------------------------------------------------- /images/6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/6.gif -------------------------------------------------------------------------------- /images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectstorm/react-canvas/cafc8a5d04342a554a458b0463c66917cc04131c/images/screenshot.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@projectstorm/react-canvas", 3 | "version": "0.0.1", 4 | "main": "./dist/main.js", 5 | "typings": "./dist/@types/src/main", 6 | "scripts": { 7 | "pretty": "prettier --use-tabs --write \"{src,demos,tests}/**/*.{ts,tsx}\" --print-width 120", 8 | "storybook": "start-storybook -p 9001 -c .storybook", 9 | "storybook:build": "build-storybook -c .storybook -o .out", 10 | "storybook:github": "storybook-to-ghpages" 11 | }, 12 | "dependencies": { 13 | "@projectstorm/react-core": "^1.2.11", 14 | "lodash": "^4.17.11", 15 | "mathjs": "^5.1.2", 16 | "react": "^16.5.1" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7", 20 | "@storybook/addon-actions": "^4.0.0-alpha.21", 21 | "@storybook/addon-knobs": "^4.0.0-alpha.21", 22 | "@storybook/addon-options": "^4.0.0-alpha.21", 23 | "@storybook/addon-storysource": "^4.0.0-alpha.21", 24 | "@storybook/addons": "^4.0.0-alpha.21", 25 | "@storybook/react": "^4.0.0-alpha.21", 26 | "@types/lodash": "^4.14.116", 27 | "@types/mathjs": "^4.4.1", 28 | "@types/react": "^16.4.14", 29 | "@types/react-dom": "^16.0.7", 30 | "awesome-typescript-loader": "5", 31 | "babel-loader": "^8.0.2", 32 | "css-loader": "^1.0.0", 33 | "file-loader": "^2.0.0", 34 | "moment": "^2.22.2", 35 | "node-sass": "^4.9.3", 36 | "prettier": "^1.14.2", 37 | "react-dom": "^16.5.1", 38 | "sass-loader": "^7.1.0", 39 | "source-map-loader": "^0.2.4", 40 | "style-loader": "^0.23.0", 41 | "ts-loader": "^5.1.1", 42 | "tsconfig-paths-webpack-plugin": "^3.2.0", 43 | "typescript": "^3.0.3", 44 | "webpack": "^4.19.0", 45 | "webpack-cli": "^3.1.0", 46 | "webpack-node-externals": "^1.7.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AbstractElementFactory.ts: -------------------------------------------------------------------------------- 1 | import { CanvasEngine } from "./CanvasEngine"; 2 | import { AbstractState } from "./state-machine/AbstractState"; 3 | import { BaseModel } from "./base-models/BaseModel"; 4 | 5 | export abstract class AbstractElementFactory { 6 | public type: string; 7 | protected directiveProcessors; 8 | protected engine: CanvasEngine; 9 | 10 | constructor(type: string) { 11 | this.type = type; 12 | this.directiveProcessors = []; 13 | } 14 | 15 | setEngine(engine: CanvasEngine) { 16 | this.engine = engine; 17 | } 18 | 19 | getCanvasStates(): AbstractState[] { 20 | return []; 21 | } 22 | 23 | abstract generateModel(): T; 24 | 25 | abstract generateWidget(engine: CanvasEngine, model: T): JSX.Element; 26 | } 27 | -------------------------------------------------------------------------------- /src/CanvasEngine.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { AbstractElementFactory } from "./AbstractElementFactory"; 3 | import { RectangleElementFactory } from "./primitives/rectangle/RectangleElementFactory"; 4 | import { CanvasModel } from "./models-canvas/CanvasModel"; 5 | import { CanvasWidget } from "./widgets/CanvasWidget"; 6 | import { SelectionElementFactory } from "./primitives/selection/SelectionElementFactory"; 7 | import { StateMachine } from "./state-machine/StateMachine"; 8 | import { TranslateCanvasState } from "./state-machine/states/TranslateCanvasState"; 9 | import { GridElementFactory } from "./primitives/grid/GridElementFactory"; 10 | import { EllipseElementFactory } from "./primitives/ellipse/EllipseElementFactory"; 11 | import { TranslateElementState } from "./state-machine/states/TranslateElementState"; 12 | import { SelectElementsState } from "./state-machine/states/SelectElementsState"; 13 | import { HistoryBank } from "./history/HistoryBank"; 14 | import { CanvasLayerFactory } from "./CanvasLayerFactory"; 15 | import { EventBus } from "./event-bus/EventBus"; 16 | import { ZoomCanvasAction } from "./event-bus/actions/ZoomCanvasAction"; 17 | import { MouseDownInput } from "./state-machine/input/MouseDownInput"; 18 | import { KeyInput } from "./state-machine/input/KeyInput"; 19 | import { ModelElementInput } from "./state-machine/input/ModelElementInput"; 20 | import { DefaultState } from "./state-machine/states/DefaultState"; 21 | import { Toolkit } from "@projectstorm/react-core"; 22 | import { CanvasLayerModel } from "./models-canvas/CanvasLayerModel"; 23 | import { SelectionElementModel } from "./primitives/selection/SelectionElementModel"; 24 | import { ModelEvent } from "./event-bus/events/ModelEvent"; 25 | import { InlineAction } from "./event-bus/InlineAction"; 26 | import { PaperElementFactory } from "./primitives/paper/PaperElementFactory"; 27 | import { BaseEvent, BaseObject } from "@projectstorm/react-core"; 28 | import { BaseModel, DeserializeEvent } from "./base-models/BaseModel"; 29 | import { EllipseElementModel } from "./primitives/ellipse/EllipseElementModel"; 30 | import { DeselectModelsAction } from "./event-bus/actions/DeselectModelsAction"; 31 | 32 | export class CanvasEngineError extends Error {} 33 | 34 | export interface CanvasEngineListener { 35 | modelChanged?: (event: BaseEvent & { model: T; oldModel: T }) => any; 36 | } 37 | 38 | export class CanvasEngine extends BaseObject> { 39 | protected elementFactories: { [type: string]: AbstractElementFactory }; 40 | protected model: T; 41 | protected stateMachine: StateMachine; 42 | protected canvasWidget; 43 | protected historyBank: HistoryBank; 44 | protected eventBus: EventBus; 45 | protected debugMode: boolean; 46 | 47 | private modelListener: string; 48 | debugLayer: CanvasLayerModel; 49 | 50 | constructor() { 51 | super(); 52 | this.elementFactories = {}; 53 | this.model = null; 54 | this.canvasWidget = null; 55 | this.stateMachine = new StateMachine(); 56 | this.historyBank = new HistoryBank(); 57 | this.eventBus = new EventBus(); 58 | this.modelListener = null; 59 | this.debugMode = false; 60 | 61 | if (Toolkit.TESTING) { 62 | Toolkit.TESTING_UID = 0; 63 | } 64 | } 65 | 66 | enableDebugMode(debug: boolean) { 67 | this.debugMode = debug; 68 | if (debug) { 69 | // debug layer 70 | this.debugLayer = new CanvasLayerModel(); 71 | this.debugLayer.setSVG(true); 72 | this.debugLayer.setTransformable(true); 73 | } 74 | } 75 | 76 | getEventBus(): EventBus { 77 | return this.eventBus; 78 | } 79 | 80 | getHistoryBank(): HistoryBank { 81 | return this.historyBank; 82 | } 83 | 84 | getStateMachine(): StateMachine { 85 | return this.stateMachine; 86 | } 87 | 88 | setModel(model: T) { 89 | // uninstall the old model 90 | if (this.modelListener) { 91 | this.model.removeListener(this.modelListener); 92 | } 93 | let oldModel = this.model; 94 | this.model = model; 95 | this.iterateListeners("Model changed", (listener, event) => { 96 | if (listener.modelChanged) { 97 | listener.modelChanged({ ...event, model: model, oldModel: oldModel }); 98 | } 99 | }); 100 | 101 | // install the new model 102 | if (model) { 103 | this.modelListener = model.addListener({ 104 | delegateEvent: event => { 105 | this.eventBus.fireEvent(new ModelEvent(event)); 106 | } 107 | }); 108 | } else { 109 | this.modelListener = null; 110 | } 111 | } 112 | 113 | getModel(): T { 114 | return this.model; 115 | } 116 | 117 | generateEntityFor(type: string): BaseModel { 118 | return this.elementFactories[type].generateModel(); 119 | } 120 | 121 | deserialize(data: any) { 122 | let event = new DeserializeEvent(data, this); 123 | this.model.deSerialize(event); 124 | this.canvasWidget.forceUpdate(); 125 | } 126 | 127 | installHistoryBank() { 128 | this.stateMachine.addListener({ 129 | stateChanged: event => { 130 | if (this.model) { 131 | this.historyBank.pushState(this.model.serialize()); 132 | } 133 | } 134 | }); 135 | this.historyBank.addListener({ 136 | forward: event => { 137 | this.deserialize(event.state); 138 | }, 139 | backward: event => { 140 | this.deserialize(event.state); 141 | } 142 | }); 143 | } 144 | 145 | repaint() { 146 | if (this.canvasWidget) { 147 | if (this.debugMode) { 148 | this.model.layers.moveModelToFront(this.debugLayer); 149 | this.debugLayer.clearEntities(); 150 | _.forEach(this.model.getElements(), element => { 151 | let dimensions = element.getDimensions(); 152 | if (dimensions) { 153 | this.debugLayer.addModels( 154 | _.map( 155 | EllipseElementModel.createPointCloudFrom(dimensions, 3 / this.model.getZoomLevel()), 156 | point => { 157 | point.background = "mediumpurple"; 158 | return point; 159 | } 160 | ) 161 | ); 162 | } 163 | }); 164 | } 165 | 166 | this.canvasWidget.forceUpdate(); 167 | } 168 | } 169 | 170 | installDefaultInteractivity() { 171 | // selection layer 172 | let selectionLayer = new CanvasLayerModel(); 173 | selectionLayer.setSVG(false); 174 | selectionLayer.setTransformable(false); 175 | 176 | // listen for a new model 177 | this.addListener({ 178 | modelChanged: event => { 179 | if (event.oldModel) { 180 | event.oldModel.removeLayer(selectionLayer); 181 | if (this.debugLayer) { 182 | event.oldModel.removeLayer(this.debugLayer); 183 | } 184 | } 185 | if (event.model) { 186 | event.model.addLayer(selectionLayer); 187 | if (this.debugLayer) { 188 | event.model.addLayer(this.debugLayer); 189 | } 190 | } 191 | } 192 | }); 193 | 194 | this.eventBus.registerAction( 195 | new InlineAction(ModelEvent.NAME, (event: ModelEvent) => { 196 | // setup a combo box for when there are models 197 | if (event.modelEvent.name === "selection changed") { 198 | selectionLayer.clearEntities(); 199 | this.model.layers.moveModelToFront(selectionLayer); 200 | let selected = _.filter(this.model.getElements(), element => { 201 | return element.isSelected(); 202 | }); 203 | if (selected.length > 0) { 204 | let model = new SelectionElementModel(); 205 | model.setModels(selected); 206 | selectionLayer.addModel(model); 207 | this.canvasWidget.forceUpdate(); 208 | } 209 | } 210 | }) 211 | ); 212 | } 213 | 214 | registerElementFactory(factory: AbstractElementFactory) { 215 | this.elementFactories[factory.type] = factory; 216 | factory.setEngine(this); 217 | _.forEach(factory.getCanvasStates(), state => { 218 | this.stateMachine.addState(state); 219 | }); 220 | } 221 | 222 | installDefaults() { 223 | // element factories 224 | this.registerElementFactory(new CanvasLayerFactory()); 225 | this.registerElementFactory(new RectangleElementFactory()); 226 | this.registerElementFactory(new SelectionElementFactory()); 227 | this.registerElementFactory(new GridElementFactory()); 228 | this.registerElementFactory(new EllipseElementFactory()); 229 | this.registerElementFactory(new PaperElementFactory()); 230 | 231 | // install actions 232 | KeyInput.installActions(this.stateMachine, this.eventBus); 233 | MouseDownInput.installActions(this.stateMachine, this.eventBus); 234 | ModelElementInput.installActions(this.stateMachine, this.eventBus); 235 | this.eventBus.registerAction(new ZoomCanvasAction(this)); 236 | this.eventBus.registerAction(new DeselectModelsAction(this)); 237 | 238 | // possible states 239 | this.stateMachine.addState(new SelectElementsState(this)); 240 | this.stateMachine.addState(new TranslateElementState(this)); 241 | this.stateMachine.addState(new TranslateCanvasState(this)); 242 | this.stateMachine.addState(new DefaultState(this)); 243 | 244 | // default wiring 245 | this.installHistoryBank(); 246 | this.installDefaultInteractivity(); 247 | 248 | // process to set the initial state 249 | this.stateMachine.process(); 250 | } 251 | 252 | getCanvasWidget(): CanvasWidget { 253 | return this.canvasWidget; 254 | } 255 | 256 | setCanvasWidget(widget: CanvasWidget) { 257 | this.canvasWidget = widget; 258 | if (widget) { 259 | this.historyBank.pushState(this.model.serialize()); 260 | } 261 | } 262 | 263 | getFactory(type: string): AbstractElementFactory { 264 | if (!this.elementFactories[type]) { 265 | throw new CanvasEngineError("Cannot find Element factory with type: " + type); 266 | } 267 | return this.elementFactories[type]; 268 | } 269 | 270 | getFactoryForElement(element: BaseModel): AbstractElementFactory { 271 | return this.getFactory(element.getType()); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/CanvasLayerFactory.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { AbstractElementFactory } from "./AbstractElementFactory"; 3 | import { CanvasLayerModel } from "./models-canvas/CanvasLayerModel"; 4 | import { CanvasEngine } from "./CanvasEngine"; 5 | import { CanvasLayerWidget } from "./widgets/CanvasLayerWidget"; 6 | 7 | export class CanvasLayerFactory extends AbstractElementFactory { 8 | constructor() { 9 | super("layer"); 10 | } 11 | 12 | generateModel(): CanvasLayerModel { 13 | return new CanvasLayerModel(); 14 | } 15 | 16 | generateWidget(engine: CanvasEngine, model: CanvasLayerModel): JSX.Element { 17 | return ; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/base-models/BaseModel.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent, BaseListener, BaseObject, Toolkit } from "@projectstorm/react-core"; 2 | import { CanvasEngine } from "../CanvasEngine"; 3 | 4 | export interface Serializable { 5 | _type: string; 6 | id: string; 7 | } 8 | 9 | export interface BaseModelListener extends BaseListener { 10 | lockChanged?(event: BaseEvent & { locked: boolean }); 11 | delegateEvent?(event: BaseEvent); 12 | } 13 | 14 | export class DeserializeEvent { 15 | data: { [p: string]: any }; 16 | engine: CanvasEngine; 17 | cache: { [id: string]: BaseModel }; 18 | 19 | constructor(data: any, engine: CanvasEngine) { 20 | this.cache = {}; 21 | this.data = data; 22 | this.engine = engine; 23 | } 24 | 25 | subset(key: string): DeserializeEvent { 26 | let event = new DeserializeEvent(this.data[key], this.engine); 27 | event.cache = this.cache; 28 | return event; 29 | } 30 | } 31 | 32 | export class BaseModel< 33 | PARENT extends BaseModel = any, 34 | LISTENER extends BaseModelListener = BaseListener 35 | > extends BaseObject { 36 | protected parent: PARENT; 37 | protected id: string; 38 | protected type: string; 39 | protected locked: boolean; 40 | private parentListener: string; 41 | 42 | constructor(type: string) { 43 | super(); 44 | this.id = Toolkit.UID(); 45 | this.type = type; 46 | this.parentListener = null; 47 | } 48 | 49 | isLocked(): boolean { 50 | return this.locked || (this.parent && this.parent.isLocked()); 51 | } 52 | 53 | setParent(parent: PARENT) { 54 | if (this.parentListener) { 55 | this.parent.removeListener(this.parentListener); 56 | } 57 | this.parent = parent; 58 | if (parent) { 59 | this.parentListener = parent.addListener({ 60 | delegateEvent: event => { 61 | if (parent.parent) { 62 | parent.parent.iterateListeners("delegating event", listener => { 63 | if (listener.delegateEvent) { 64 | listener.delegateEvent(event); 65 | } 66 | }); 67 | } 68 | } 69 | }); 70 | } 71 | } 72 | 73 | getType(): string { 74 | return this.type; 75 | } 76 | 77 | getParent(): PARENT { 78 | return this.parent; 79 | } 80 | 81 | public clearListeners() { 82 | this.listeners = {}; 83 | } 84 | 85 | iterateListeners(name: string, cb: (t: LISTENER, event: BaseEvent) => any) { 86 | // optionally delegate the event up the stack so the event bus can grab it 87 | if (this.parent) { 88 | this.parent.iterateListeners(name, (listener, event) => { 89 | if (listener.delegateEvent) { 90 | listener.delegateEvent(event); 91 | } 92 | }); 93 | } 94 | return super.iterateListeners(name, cb); 95 | } 96 | 97 | public deSerialize(event: DeserializeEvent) { 98 | this.id = event.data.id; 99 | this.locked = !!event.data.locked; 100 | if (event.data["parent"]) { 101 | if (!event.cache[event.data["parent"]]) { 102 | throw "Cannot deserialize, because of missing parent"; 103 | } 104 | this.setParent(event.cache[event.data["parent"]] as any); 105 | } 106 | event.cache[this.id] = this; 107 | } 108 | 109 | public serialize(): Serializable & any { 110 | return { 111 | _type: this.type, 112 | id: this.id, 113 | parent: this.parent && this.parent.id, 114 | locked: this.locked 115 | }; 116 | } 117 | 118 | public getID() { 119 | return this.id; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/base-models/GraphModel.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, BaseModelListener, DeserializeEvent, Serializable } from "./BaseModel"; 2 | import * as _ from "lodash"; 3 | import { BaseEvent } from "@projectstorm/react-core"; 4 | 5 | export interface GraphModelListener extends BaseModelListener { 6 | modelsAdded?: (event: BaseEvent & { models: CHILD[] }) => any; 7 | 8 | modelsRemoved?: (event: BaseEvent & { models: CHILD[] }) => any; 9 | } 10 | 11 | /** 12 | * Model that supports graph traversal 13 | */ 14 | export class GraphModel< 15 | CHILD extends BaseModel = BaseModel, 16 | PARENT extends BaseModel = BaseModel, 17 | LISTENER extends GraphModelListener = GraphModelListener 18 | > extends BaseModel { 19 | protected children: { [id: string]: CHILD }; 20 | protected parentDelegate: BaseModel; 21 | 22 | constructor(type: string = "graph") { 23 | super(type); 24 | this.children = {}; 25 | this.parentDelegate = this; 26 | } 27 | 28 | setParentDelegate(parent: BaseModel) { 29 | this.parentDelegate = parent; 30 | } 31 | 32 | count(): number { 33 | return _.values(this.children).length; 34 | } 35 | 36 | addModels(entities: CHILD[]) { 37 | _.forEach(entities, entity => { 38 | this.children[entity.getID()] = entity; 39 | entity.setParent(this.parentDelegate); 40 | }); 41 | this.iterateListeners("children added", (listener, event) => { 42 | if (listener.modelsAdded) { 43 | listener.modelsAdded({ ...event, models: entities }); 44 | } 45 | }); 46 | } 47 | 48 | addModel(entity: CHILD) { 49 | this.addModels([entity]); 50 | } 51 | 52 | removeModels(entities: CHILD[]) { 53 | _.forEach(entities, entity => { 54 | delete this.children[entity.getID()]; 55 | entity.setParent(null); 56 | }); 57 | 58 | this.iterateListeners("children removed", (listener, event) => { 59 | if (listener.modelsRemoved) { 60 | listener.modelsRemoved({ ...event, models: entities }); 61 | } 62 | }); 63 | } 64 | 65 | removeModel(entity: CHILD | string) { 66 | if (typeof entity === "string") { 67 | entity = this.getModel(entity); 68 | } 69 | this.removeModels([entity]); 70 | } 71 | 72 | getModel(id: string): CHILD { 73 | return this.children[id]; 74 | } 75 | 76 | serialize(): Serializable { 77 | return { 78 | ...super.serialize(), 79 | entities: _.mapValues(this.children, value => { 80 | return value.serialize(); 81 | }) 82 | }; 83 | } 84 | 85 | deSerialize(event: DeserializeEvent): void { 86 | super.deSerialize(event); 87 | let entities = event.subset("entities"); 88 | this.children = _.mapValues(entities.data, (entity: any, index) => { 89 | let entityOb = event.engine.generateEntityFor(entity._type); 90 | entityOb.deSerialize(entities.subset(index)); 91 | return entityOb; 92 | }) as any; 93 | } 94 | 95 | getEntities(): { [id: string]: CHILD } { 96 | return this.children; 97 | } 98 | 99 | clearEntities() { 100 | this.removeModels(_.values(this.children)); 101 | } 102 | 103 | getAllEntities(): CHILD[] { 104 | return _.flatMap(this.children, entity => { 105 | let arr = [entity]; 106 | if (entity instanceof GraphModel) { 107 | arr = arr.concat(entity.getAllEntities()); 108 | } 109 | return arr; 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/base-models/GraphModelOrdered.ts: -------------------------------------------------------------------------------- 1 | import { GraphModel, GraphModelListener } from "./GraphModel"; 2 | import { BaseModel, DeserializeEvent } from "./BaseModel"; 3 | import * as _ from "lodash"; 4 | 5 | export class GraphModelOrdered< 6 | CHILD extends BaseModel, 7 | PARENT extends BaseModel, 8 | LISTENER extends GraphModelListener = any 9 | > extends GraphModel { 10 | protected entitiesOrdered: CHILD[]; 11 | 12 | constructor(type: string = "graph") { 13 | super(type); 14 | this.entitiesOrdered = []; 15 | } 16 | 17 | getArray(): CHILD[] { 18 | return this.entitiesOrdered; 19 | } 20 | 21 | serialize() { 22 | return { 23 | ...super.serialize(), 24 | entitiesOrdered: _.map(this.entitiesOrdered, entity => { 25 | return entity.getID(); 26 | }) 27 | }; 28 | } 29 | 30 | deSerialize(event: DeserializeEvent): void { 31 | super.deSerialize(event); 32 | this.entitiesOrdered = _.map(event.data["entitiesOrdered"], entityID => { 33 | return this.children[entityID]; 34 | }); 35 | } 36 | 37 | addModels(entities: CHILD[], position?: number) { 38 | super.addModels(entities); 39 | if (position == null) { 40 | this.entitiesOrdered = this.entitiesOrdered.concat(entities); 41 | } else { 42 | this.entitiesOrdered.splice(position, 0, ...entities); 43 | } 44 | } 45 | 46 | addModel(entity: CHILD, position?: number) { 47 | this.addModels([entity], position); 48 | } 49 | 50 | removeModels(entities: CHILD[]) { 51 | for (let i = this.entitiesOrdered.length; i >= 0; i--) { 52 | let index = this.entitiesOrdered.indexOf(this.entitiesOrdered[i]); 53 | if (index !== -1) { 54 | this.entitiesOrdered.splice(index, 1); 55 | } 56 | } 57 | super.removeModels(entities); 58 | } 59 | 60 | moveModelToBack(element: CHILD) { 61 | let index = this.entitiesOrdered.indexOf(element); 62 | if (index === -1) { 63 | return; 64 | } 65 | this.entitiesOrdered.splice(0, 0, element); 66 | } 67 | 68 | moveModelToFront(element: CHILD) { 69 | let index = this.entitiesOrdered.indexOf(element); 70 | if (index === -1) { 71 | return; 72 | } 73 | this.entitiesOrdered.splice(index, 1); 74 | this.entitiesOrdered.push(element); 75 | } 76 | 77 | moveModel(element: CHILD, newIndex: number) { 78 | let index = this.entitiesOrdered.indexOf(element); 79 | if (index === -1) { 80 | return; 81 | } 82 | this.entitiesOrdered.splice(index, 1); 83 | this.entitiesOrdered.splice(newIndex, 0, element); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/event-bus/Action.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "./Event"; 2 | import { Toolkit } from "@projectstorm/react-core"; 3 | 4 | export abstract class Action { 5 | targetEvent: string; 6 | id: string; 7 | 8 | constructor(targetEvent: string) { 9 | this.targetEvent = targetEvent; 10 | this.id = Toolkit.UID(); 11 | } 12 | 13 | abstract doAction(event: T); 14 | } 15 | -------------------------------------------------------------------------------- /src/event-bus/Event.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "./Action"; 2 | 3 | export class Event { 4 | stopped: boolean; 5 | source: any; 6 | name: string; 7 | actionsFired: Action[]; 8 | 9 | constructor(name: string, source: any) { 10 | this.name = name; 11 | this.source = source; 12 | this.stopped = false; 13 | this.actionsFired = []; 14 | } 15 | 16 | fire(action: Action) { 17 | action.doAction(this); 18 | this.actionsFired.push(action); 19 | } 20 | 21 | stopPropagation() { 22 | this.stopped = true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/event-bus/EventBus.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "./Action"; 2 | import { Event } from "./Event"; 3 | import * as _ from "lodash"; 4 | import { BaseEvent, BaseListener, BaseObject } from "@projectstorm/react-core"; 5 | 6 | export interface EventBusListener extends BaseListener { 7 | eventWillFire?: (event: BaseEvent & { event: Event }) => any; 8 | eventDidFire?: (event: BaseEvent & { event: Event }) => any; 9 | } 10 | 11 | export class EventBus extends BaseObject { 12 | actions: { 13 | [targetEvent: string]: { 14 | [id: string]: Action; 15 | }; 16 | }; 17 | 18 | constructor() { 19 | super(); 20 | this.actions = {}; 21 | } 22 | 23 | unRegisterAction(action: Action) { 24 | if (!this.actions[action.targetEvent]) { 25 | return; 26 | } 27 | 28 | if (!this.actions[action.targetEvent][action.id]) { 29 | return; 30 | } 31 | 32 | delete this.actions[action.targetEvent][action.id]; 33 | 34 | if (_.keys(this.actions[action.targetEvent]).length === 0) { 35 | delete this.actions[action.targetEvent]; 36 | } 37 | } 38 | 39 | registerAction(action: Action): Action { 40 | if (!this.actions[action.targetEvent]) { 41 | this.actions[action.targetEvent] = {}; 42 | } 43 | this.actions[action.targetEvent][action.id] = action; 44 | return action; 45 | } 46 | 47 | fireEvent(event: Event) { 48 | if (!this.actions[event.name]) { 49 | return; 50 | } 51 | 52 | // before the event fires 53 | this.iterateListeners("event will fire", (listener, baseEvent) => { 54 | if (listener.eventWillFire) { 55 | listener.eventWillFire({ 56 | ...baseEvent, 57 | event: event 58 | }); 59 | } 60 | }); 61 | 62 | let processedActions = {}; 63 | do { 64 | _.some(this.actions[event.name], action => { 65 | if (event.stopped) { 66 | return true; 67 | } 68 | processedActions[action.id] = action; 69 | event.fire(action); 70 | }); 71 | } while (!event.stopped && _.keys(this.actions[event.name]).length !== _.keys(processedActions).length); 72 | 73 | if (event.actionsFired.length > 0) { 74 | this.iterateListeners("event did fire", (listener, baseEvent) => { 75 | if (listener.eventDidFire) { 76 | listener.eventDidFire({ 77 | ...baseEvent, 78 | event: event 79 | }); 80 | } 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/event-bus/InlineAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "./Action"; 2 | import { Event } from "./Event"; 3 | 4 | export class InlineAction extends Action { 5 | cb: (event: T) => any; 6 | 7 | constructor(name: string, callback: (event: T) => any) { 8 | super(name); 9 | this.cb = callback; 10 | } 11 | 12 | doAction(event: T) { 13 | return this.cb(event); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/event-bus/actions/DeselectModelsAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "../Action"; 2 | import { KeyDownEvent } from "../events/key"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import * as _ from "lodash"; 5 | 6 | export class DeselectModelsAction extends Action { 7 | engine: CanvasEngine; 8 | 9 | constructor(engine: CanvasEngine) { 10 | super(KeyDownEvent.NAME); 11 | this.engine = engine; 12 | } 13 | 14 | doAction(event: KeyDownEvent) { 15 | if (event.key === "Escape") { 16 | let entities = this.engine.getModel().getSelectedEntities(); 17 | _.forEach(entities, entity => { 18 | entity.setSelected(false); 19 | }); 20 | this.engine.repaint(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/event-bus/actions/SelectCanvasAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "../Action"; 2 | import { MouseDownEvent } from "../events/mouse"; 3 | import * as _ from "lodash"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | 6 | export class SelectCanvasAction extends Action { 7 | engine: CanvasEngine; 8 | 9 | constructor(engine: CanvasEngine) { 10 | super(MouseDownEvent.NAME); 11 | this.engine = engine; 12 | } 13 | 14 | doAction(event: MouseDownEvent) { 15 | event.stopPropagation(); 16 | let entities = this.engine.getModel().getSelectedEntities(); 17 | _.forEach(entities, entity => { 18 | entity.setSelected(false); 19 | }); 20 | this.engine.repaint(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/event-bus/actions/SelectElementAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "../Action"; 2 | import { PressElementEvent } from "../events/elements"; 3 | import * as _ from "lodash"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | 6 | export class SelectElementAction extends Action { 7 | engine: CanvasEngine; 8 | selectMultiple: boolean; 9 | 10 | constructor(engine: CanvasEngine, selectMultiple: boolean = false) { 11 | super(PressElementEvent.NAME); 12 | this.engine = engine; 13 | this.selectMultiple = selectMultiple; 14 | } 15 | 16 | doAction(event: PressElementEvent) { 17 | event.stopPropagation(); 18 | if (!event.element.isSelected()) { 19 | if (!this.selectMultiple) { 20 | _.forEach(this.engine.getModel().getSelectedEntities(), entity => { 21 | entity.setSelected(false); 22 | }); 23 | } 24 | event.element.setSelected(true); 25 | this.engine.repaint(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/event-bus/actions/ZoomCanvasAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "../Action"; 2 | import { MouseWheelEvent } from "../events/mouse"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | 5 | export class ZoomCanvasAction extends Action { 6 | engine: CanvasEngine; 7 | 8 | constructor(engine: CanvasEngine) { 9 | super(MouseWheelEvent.NAME); 10 | this.engine = engine; 11 | } 12 | 13 | doAction(event: MouseWheelEvent) { 14 | const model = this.engine.getModel(); 15 | const canvas = this.engine.getCanvasWidget(); 16 | 17 | let newZoomFactor = model.getZoomLevel() + event.amount / 100.0; 18 | if (newZoomFactor <= 0.1) { 19 | return; 20 | } 21 | 22 | const oldZoomFactor = model.getZoomLevel(); 23 | 24 | const boundingRect = canvas.dimension.realDimensions; 25 | const clientWidth = boundingRect.getWidth(); 26 | const clientHeight = boundingRect.getHeight(); 27 | 28 | // compute difference between rect before and after scroll 29 | const widthDiff = clientWidth * newZoomFactor - clientWidth * oldZoomFactor; 30 | const heightDiff = clientHeight * newZoomFactor - clientHeight * oldZoomFactor; 31 | 32 | // compute mouse coords relative to canvas 33 | const clientX = event.mouseX - boundingRect.getTopLeft().x; 34 | const clientY = event.mouseY - boundingRect.getTopLeft().y; 35 | 36 | // compute width and height increment factor 37 | const xFactor = (clientX - model.getOffsetX()) / oldZoomFactor / clientWidth; 38 | const yFactor = (clientY - model.getOffsetY()) / oldZoomFactor / clientHeight; 39 | 40 | model.setZoomLevel(newZoomFactor); 41 | model.setOffset(model.getOffsetX() - widthDiff * xFactor, model.getOffsetY() - heightDiff * yFactor); 42 | this.engine.repaint(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/event-bus/events/ModelEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "../Event"; 2 | import { BaseEvent } from "@projectstorm/react-core"; 3 | 4 | export class ModelEvent extends Event { 5 | modelEvent: BaseEvent; 6 | 7 | static NAME = "model-delegate-event"; 8 | 9 | constructor(modelEvent: BaseEvent) { 10 | super(ModelEvent.NAME, modelEvent.source); 11 | this.modelEvent = modelEvent; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/event-bus/events/elements.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "../Event"; 2 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 3 | 4 | export class ElementEvent extends Event { 5 | element: CanvasElementModel; 6 | 7 | constructor(name: string, source: any, element: CanvasElementModel) { 8 | super(name, source); 9 | this.element = element; 10 | } 11 | } 12 | 13 | export class PressElementEvent extends ElementEvent { 14 | static NAME = "press-element"; 15 | 16 | constructor(source: any, element: CanvasElementModel) { 17 | super(PressElementEvent.NAME, source, element); 18 | } 19 | } 20 | 21 | export class UnPressElementEvent extends ElementEvent { 22 | static NAME = "unpress-element"; 23 | 24 | constructor(source: any, element: CanvasElementModel) { 25 | super(UnPressElementEvent.NAME, source, element); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/event-bus/events/key.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "../Event"; 2 | 3 | export abstract class KeyEvent extends Event { 4 | key: string; 5 | 6 | constructor(name: string, source: any, key: string) { 7 | super(name, source); 8 | this.key = key; 9 | } 10 | } 11 | 12 | export class KeyDownEvent extends KeyEvent { 13 | static NAME = "key-down"; 14 | 15 | constructor(source: any, key: string) { 16 | super(KeyDownEvent.NAME, source, key); 17 | } 18 | } 19 | 20 | export class KeyUpEvent extends KeyEvent { 21 | static NAME = "key-up"; 22 | 23 | constructor(source: any, key: string) { 24 | super(KeyUpEvent.NAME, source, key); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/event-bus/events/mouse.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "../Event"; 2 | import { Point } from "../../geometry/Point"; 3 | import * as _ from "lodash"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | 6 | export abstract class MouseEvent extends Event { 7 | mouseX: number; 8 | mouseY: number; 9 | 10 | constructor(name: string, source: any, mouseX: number, mouseY: number) { 11 | super(name, source); 12 | this.mouseX = mouseX; 13 | this.mouseY = mouseY; 14 | } 15 | 16 | getCanvasCoordinates(engine: CanvasEngine): { x: number; y: number } { 17 | let model = engine.getModel(); 18 | let canDimensions = engine.getCanvasWidget().dimension.realDimensions; 19 | 20 | return { 21 | x: (this.mouseX - canDimensions.getTopLeft().x - model.getOffsetX()) / model.getZoomLevel(), 22 | y: (this.mouseY - canDimensions.getTopLeft().y - model.getOffsetY()) / model.getZoomLevel() 23 | }; 24 | } 25 | } 26 | 27 | export class MouseDownEvent extends MouseEvent { 28 | static NAME = "mouse-down"; 29 | 30 | constructor(source: any, mouseX: number, mouseY: number) { 31 | super(MouseDownEvent.NAME, source, mouseX, mouseY); 32 | } 33 | } 34 | 35 | export class MouseUpEvent extends MouseEvent { 36 | static NAME = "mouse-up"; 37 | 38 | constructor(source: any, mouseX: number, mouseY: number) { 39 | super(MouseUpEvent.NAME, source, mouseX, mouseY); 40 | } 41 | } 42 | 43 | export class MouseMoveEvent extends MouseEvent { 44 | static NAME = "mouse-move"; 45 | 46 | constructor(source: any, mouseX: number, mouseY: number) { 47 | super(MouseMoveEvent.NAME, source, mouseX, mouseY); 48 | } 49 | } 50 | 51 | export class MouseWheelEvent extends MouseEvent { 52 | amount: number; 53 | 54 | static NAME = "mouse-wheel"; 55 | 56 | constructor(source: any, mouseX: number, mouseY: number, amount: number) { 57 | super(MouseWheelEvent.NAME, source, mouseX, mouseY); 58 | this.amount = amount; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/geometry/Point.ts: -------------------------------------------------------------------------------- 1 | import * as mathjs from "mathjs"; 2 | import Matrix = mathjs.Matrix; 3 | 4 | export class Point { 5 | x: number; 6 | y: number; 7 | 8 | constructor(x: number, y: number) { 9 | this.x = x; 10 | this.y = y; 11 | } 12 | 13 | translate(x: number, y: number) { 14 | this.x += x; 15 | this.y += y; 16 | } 17 | 18 | clone() { 19 | return new Point(this.x, this.y); 20 | } 21 | 22 | public asMatrix() { 23 | return mathjs.matrix([[this.x], [this.y], [1]]); 24 | } 25 | 26 | transform(matrix: Matrix) { 27 | let final = mathjs.multiply(matrix, this.asMatrix()); 28 | this.x = final.get([0, 0]); 29 | this.y = final.get([1, 0]); 30 | } 31 | 32 | public static middlePoint(pointA: Point, pointB: Point): Point { 33 | return new Point((pointB.x + pointA.x) / 2, (pointB.y + pointA.y) / 2); 34 | } 35 | 36 | public static multiply(...matrices: Matrix[]): Matrix { 37 | let m: Matrix = matrices[0]; 38 | for (let i = 1; i < matrices.length; i++) { 39 | m = mathjs.multiply(m, matrices[i]); 40 | } 41 | return m; 42 | } 43 | 44 | public static scaleMatrix(x: number, y: number): Matrix { 45 | return mathjs.matrix([[x, 0, 0], [0, y, 0], [0, 0, 1]]); 46 | } 47 | 48 | public static translateMatrix(x: number, y: number): Matrix { 49 | return mathjs.matrix([[1, 0, x], [0, 1, y], [0, 0, 1]]); 50 | } 51 | 52 | public static rotateMatrix(deg: number): Matrix { 53 | return mathjs.matrix([[Math.cos(deg), -1 * Math.sin(deg), 0], [Math.sin(deg), Math.cos(deg), 0], [0, 0, 1]]); 54 | } 55 | 56 | static createScaleMatrix(x, y, origin: Point): Matrix { 57 | return this.multiply( 58 | Point.translateMatrix(origin.x, origin.y), 59 | Point.scaleMatrix(x, y), 60 | Point.translateMatrix(-origin.x, -origin.y) 61 | ); 62 | } 63 | 64 | static createRotateMatrix(deg: number, origin: Point): Matrix { 65 | return this.multiply( 66 | Point.translateMatrix(origin.x, origin.y), 67 | Point.rotateMatrix(deg), 68 | Point.translateMatrix(-origin.x, -origin.y) 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/geometry/Polygon.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./Point"; 2 | import { Rectangle } from "./Rectangle"; 3 | import * as _ from "lodash"; 4 | import { CanvasModel } from "../models-canvas/CanvasModel"; 5 | import { Matrix } from "mathjs"; 6 | 7 | export class Polygon { 8 | protected points: Point[]; 9 | 10 | constructor(points: Point[] = []) { 11 | this.points = points; 12 | } 13 | 14 | getSVGPoints(): string { 15 | return _ 16 | .map(this.points, point => { 17 | return point.x + "," + point.y; 18 | }) 19 | .join(" "); 20 | } 21 | 22 | serialize() { 23 | return _.map(this.points, point => { 24 | return [point.x, point.y]; 25 | }); 26 | } 27 | 28 | deserialize(data: any) { 29 | this.points = _.map(data, point => { 30 | return new Point(point[0], point[1]); 31 | }); 32 | } 33 | 34 | scale(x, y, origin: Point) { 35 | let matrix = Point.createScaleMatrix(x, y, origin); 36 | _.forEach(this.points, point => { 37 | point.transform(matrix); 38 | }); 39 | } 40 | 41 | transform(matrix: Matrix) { 42 | _.forEach(this.points, point => { 43 | point.transform(matrix); 44 | }); 45 | } 46 | 47 | setPoints(points: Point[]) { 48 | this.points = points; 49 | } 50 | 51 | getPoints(): Point[] { 52 | return this.points; 53 | } 54 | 55 | translate(offsetX: number, offsetY: number) { 56 | _.forEach(this.points, point => { 57 | point.translate(offsetX, offsetY); 58 | }); 59 | } 60 | 61 | doClone(ob: this) { 62 | this.points = _.map(ob.points, point => { 63 | return point.clone(); 64 | }); 65 | } 66 | 67 | clone(): this { 68 | let ob = Object.create(this); 69 | ob.doClone(this); 70 | return ob; 71 | } 72 | 73 | toRealDimensions(model: CanvasModel): this { 74 | let dim = this.clone(); 75 | dim.scale(model.getZoomLevel(), model.getZoomLevel(), new Point(0, 0)); 76 | dim.translate(model.offsetX, model.offsetY); 77 | return dim; 78 | } 79 | 80 | getOrigin(): Point { 81 | if (this.points.length === 0) { 82 | return null; 83 | } 84 | let dimensions = this.getBoundingBox(); 85 | return Point.middlePoint(dimensions.getTopLeft(), dimensions.getBottomRight()); 86 | } 87 | 88 | static boundingBoxFromPolygons(polygons: Polygon[]): Rectangle { 89 | return Polygon.boundingBoxFromPoints( 90 | _.flatMap(polygons, polygon => { 91 | return polygon.getPoints(); 92 | }) 93 | ); 94 | } 95 | 96 | static boundingBoxFromPoints(points: Point[]): Rectangle { 97 | let minX = points[0].x; 98 | let maxX = points[0].x; 99 | let minY = points[0].y; 100 | let maxY = points[0].y; 101 | 102 | for (let i = 1; i < points.length; i++) { 103 | if (points[i].x < minX) { 104 | minX = points[i].x; 105 | } 106 | if (points[i].x > maxX) { 107 | maxX = points[i].x; 108 | } 109 | if (points[i].y < minY) { 110 | minY = points[i].y; 111 | } 112 | if (points[i].y > maxY) { 113 | maxY = points[i].y; 114 | } 115 | } 116 | 117 | return new Rectangle( 118 | new Point(minX, minY), 119 | new Point(maxX, minY), 120 | new Point(maxX, maxY), 121 | new Point(minX, maxY) 122 | ); 123 | } 124 | 125 | getBoundingBox(): Rectangle { 126 | let minX = this.points[0].x; 127 | let maxX = this.points[0].x; 128 | let minY = this.points[0].y; 129 | let maxY = this.points[0].y; 130 | 131 | for (let i = 1; i < this.points.length; i++) { 132 | if (this.points[i].x < minX) { 133 | minX = this.points[i].x; 134 | } 135 | if (this.points[i].x > maxX) { 136 | maxX = this.points[i].x; 137 | } 138 | if (this.points[i].y < minY) { 139 | minY = this.points[i].y; 140 | } 141 | if (this.points[i].y > maxY) { 142 | maxY = this.points[i].y; 143 | } 144 | } 145 | 146 | return new Rectangle( 147 | new Point(minX, minY), 148 | new Point(maxX, minY), 149 | new Point(maxX, maxY), 150 | new Point(minX, maxY) 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/geometry/Rectangle.ts: -------------------------------------------------------------------------------- 1 | import { Polygon } from "./Polygon"; 2 | import { Point } from "./Point"; 3 | 4 | export class Rectangle extends Polygon { 5 | constructor(tl: Point, tr: Point, br: Point, bl: Point); 6 | constructor(position: Point, width: number, height: number); 7 | constructor(x?: number, y?: number, width?: number, height?: number); 8 | 9 | constructor(a: any = 0, b: any = 0, c: any = 0, d: any = 0) { 10 | if (a instanceof Point && b instanceof Point && c instanceof Point && d instanceof Point) { 11 | super([a, b, c, d]); 12 | } else if (a instanceof Point) { 13 | super([a, new Point(a.x + b, a.y), new Point(a.x + b, a.y + c), new Point(a.x, a.y + c)]); 14 | } else { 15 | super(Rectangle.pointsFromBounds(a, b, c, d)); 16 | } 17 | } 18 | 19 | static pointsFromBounds(x: number, y: number, width: number, height: number): Point[] { 20 | return [new Point(x, y), new Point(x + width, y), new Point(x + width, y + height), new Point(x, y + height)]; 21 | } 22 | 23 | updateDimensions(x: number, y: number, width: number, height: number) { 24 | this.points = Rectangle.pointsFromBounds(x, y, width, height); 25 | } 26 | 27 | setPoints(points: Point[]) { 28 | if (points.length !== 4) { 29 | throw "Rectangles must always have 4 points"; 30 | } 31 | super.setPoints(points); 32 | } 33 | 34 | getWidth(): number { 35 | return Math.sqrt( 36 | Math.pow(this.getTopLeft().x - this.getTopRight().x, 2) + 37 | Math.pow(this.getTopLeft().y - this.getTopRight().y, 2) 38 | ); 39 | } 40 | 41 | getHeight(): number { 42 | return Math.sqrt( 43 | Math.pow(this.getBottomLeft().x - this.getTopLeft().x, 2) + 44 | Math.pow(this.getBottomLeft().y - this.getTopLeft().y, 2) 45 | ); 46 | } 47 | 48 | getTopMiddle(): Point { 49 | return Point.middlePoint(this.getTopLeft(), this.getTopRight()); 50 | } 51 | 52 | getBottomMiddle(): Point { 53 | return Point.middlePoint(this.getBottomLeft(), this.getBottomRight()); 54 | } 55 | 56 | getLeftMiddle(): Point { 57 | return Point.middlePoint(this.getBottomLeft(), this.getTopLeft()); 58 | } 59 | 60 | getRightMiddle(): Point { 61 | return Point.middlePoint(this.getBottomRight(), this.getTopRight()); 62 | } 63 | 64 | getTopLeft(): Point { 65 | return this.points[0]; 66 | } 67 | 68 | getTopRight(): Point { 69 | return this.points[1]; 70 | } 71 | 72 | getBottomRight(): Point { 73 | return this.points[2]; 74 | } 75 | 76 | getBottomLeft(): Point { 77 | return this.points[3]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/history/HistoryBank.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { BaseEvent, BaseListener, BaseObject } from "@projectstorm/react-core"; 3 | import { HistoryState } from "./HistoryState"; 4 | 5 | export interface HistoryBankListener extends BaseListener { 6 | forward?(event: BaseEvent & { state: HistoryState }); 7 | 8 | backward?(event: BaseEvent & { state: HistoryState }); 9 | } 10 | 11 | export class HistoryBank extends BaseObject { 12 | history: HistoryState[]; 13 | pointer: number; 14 | 15 | constructor() { 16 | super(); 17 | this.history = []; 18 | this.pointer = 0; 19 | } 20 | 21 | pushState(state: HistoryState) { 22 | // state is equal, ignore pushing it 23 | if (_.isEqual(this.history[this.pointer], state)) { 24 | return; 25 | } 26 | 27 | this.pointer++; 28 | this.history.splice(this.pointer); 29 | this.history.push(state); 30 | } 31 | 32 | goForward() { 33 | // cant go anymore forward 34 | if (this.pointer === this.history.length - 1) { 35 | return; 36 | } 37 | this.pointer++; 38 | this.iterateListeners("history moved forward", (listener, event) => { 39 | if (listener.forward) { 40 | listener.forward({ ...event, state: this.history[this.pointer] }); 41 | } 42 | }); 43 | } 44 | 45 | goBackward() { 46 | // cant go anymore backward 47 | if (this.pointer <= 0) { 48 | return; 49 | } 50 | this.pointer--; 51 | this.iterateListeners("history moved backward", (listener, event) => { 52 | if (listener.backward) { 53 | listener.backward({ ...event, state: this.history[this.pointer] }); 54 | } 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/history/HistoryState.ts: -------------------------------------------------------------------------------- 1 | export interface HistoryState { 2 | name: string; 3 | state: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | export * from "./CanvasEngine"; 2 | export * from "./AbstractElementFactory"; 3 | export * from "./CanvasLayerFactory"; 4 | 5 | export * from "./widgets/CanvasLayerWidget"; 6 | export * from "./widgets/AnchorWidget"; 7 | export * from "./primitives/selection/SelectionGroupWidget"; 8 | 9 | export * from "./geometry/Point"; 10 | export * from "./geometry/Polygon"; 11 | export * from "./geometry/Rectangle"; 12 | 13 | export * from "./base-models/BaseModel"; 14 | export * from "./base-models/GraphModel"; 15 | export * from "./base-models/GraphModelOrdered"; 16 | 17 | export * from "./models-canvas/CanvasElementModel"; 18 | export * from "./models-canvas/CanvasLayerModel"; 19 | export * from "./models-canvas/CanvasModel"; 20 | 21 | export * from "./history/HistoryBank"; 22 | 23 | export * from "./event-bus/Action"; 24 | export * from "./event-bus/InlineAction"; 25 | export * from "./event-bus/Event"; 26 | export * from "./event-bus/EventBus"; 27 | -------------------------------------------------------------------------------- /src/models-canvas/CanvasElementModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasLayerModel } from "./CanvasLayerModel"; 2 | import { Rectangle } from "../geometry/Rectangle"; 3 | import { BaseEvent } from "../base-models/BaseObject"; 4 | import { BaseModel, BaseModelListener, DeserializeEvent } from "../base-models/BaseModel"; 5 | 6 | export interface CanvasElementModelListener extends BaseModelListener { 7 | selectionChanged?(event: BaseEvent & { selected: boolean }); 8 | 9 | lockChanged?(event: BaseEvent & { locked: boolean }); 10 | } 11 | 12 | export abstract class CanvasElementModel< 13 | T extends CanvasElementModelListener = CanvasElementModelListener 14 | > extends BaseModel { 15 | protected selected: boolean; 16 | protected locked: boolean; 17 | 18 | constructor(type: string) { 19 | super(type); 20 | this.type = type; 21 | this.selected = false; 22 | this.locked = false; 23 | } 24 | 25 | serialize() { 26 | return { 27 | ...super.serialize(), 28 | selected: this.selected, 29 | locked: this.locked 30 | }; 31 | } 32 | 33 | deSerialize(event: DeserializeEvent): void { 34 | super.deSerialize(event); 35 | this.selected = !!event.data["selected"]; 36 | this.locked = !!event.data["locked"]; 37 | } 38 | 39 | setSelected(selected: boolean) { 40 | this.selected = selected; 41 | this.iterateListeners("selection changed", (listener, event: any) => { 42 | if (listener.selectionChanged) { 43 | event.selected = selected; 44 | listener.selectionChanged(event); 45 | } 46 | }); 47 | } 48 | 49 | setLocked(locked: boolean) { 50 | this.locked = locked; 51 | this.iterateListeners("lock changed", (listener, event: any) => { 52 | if (listener.lockChanged) { 53 | event.locked = locked; 54 | listener.lockChanged(event); 55 | } 56 | }); 57 | } 58 | 59 | isSelected(): boolean { 60 | return this.selected; 61 | } 62 | 63 | isLocked(): boolean { 64 | return this.getParent().isLocked(); 65 | } 66 | 67 | abstract getDimensions(): Rectangle; 68 | 69 | abstract setDimensions(dimensions: Rectangle); 70 | 71 | moveToLayer(layer: CanvasLayerModel) { 72 | if (this.parent) { 73 | this.parent.removeModel(this); 74 | } 75 | layer.addModel(this); 76 | } 77 | 78 | moveToFront() { 79 | this.parent.moveModelToFront(this); 80 | } 81 | 82 | moveToBack() { 83 | this.parent.moveModelToBack(this); 84 | } 85 | 86 | moveTo(index: number) { 87 | this.parent.moveModel(this, index); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/models-canvas/CanvasGroupModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "./CanvasElementModel"; 2 | import { Rectangle } from "../geometry/Rectangle"; 3 | 4 | export class CanvasGroupModel extends CanvasElementModel { 5 | getDimensions(): Rectangle { 6 | return undefined; 7 | } 8 | 9 | setDimensions(dimensions: Rectangle) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/models-canvas/CanvasLayerModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "./CanvasElementModel"; 2 | import { CanvasModel } from "./CanvasModel"; 3 | import { DeserializeEvent, Serializable } from "../base-models/BaseModel"; 4 | import { GraphModelOrdered } from "../base-models/GraphModelOrdered"; 5 | 6 | export class CanvasLayerModel extends GraphModelOrdered< 7 | T, 8 | CanvasModel 9 | > { 10 | protected name: string; 11 | protected svg: boolean; 12 | protected transform: boolean; 13 | 14 | constructor(name: string = "Layer") { 15 | super("layer"); 16 | this.name = name; 17 | this.svg = false; 18 | this.transform = true; 19 | } 20 | 21 | deSerialize(event: DeserializeEvent): void { 22 | super.deSerialize(event); 23 | this.name = event.data["name"]; 24 | this.svg = event.data["svg"]; 25 | this.transform = event.data["transform"]; 26 | } 27 | 28 | serialize(): Serializable & any { 29 | return { 30 | ...super.serialize(), 31 | name: this.name, 32 | svg: this.svg, 33 | transform: this.transform 34 | }; 35 | } 36 | 37 | setTransformable(transform: boolean) { 38 | this.transform = transform; 39 | } 40 | 41 | setName(name: string) { 42 | this.name = name; 43 | } 44 | 45 | setSVG(svg: boolean) { 46 | this.svg = svg; 47 | } 48 | 49 | getName() { 50 | return this.name; 51 | } 52 | 53 | isSVG() { 54 | return this.svg; 55 | } 56 | 57 | isTransformable() { 58 | return this.transform; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/models-canvas/CanvasModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasLayerModel } from "./CanvasLayerModel"; 2 | import * as _ from "lodash"; 3 | import { CanvasElementModel } from "./CanvasElementModel"; 4 | import { CanvasEngine } from "../CanvasEngine"; 5 | import { BaseModel, BaseModelListener, DeserializeEvent } from "../base-models/BaseModel"; 6 | import { BaseEvent } from "../base-models/BaseObject"; 7 | import { GraphModelOrdered } from "../base-models/GraphModelOrdered"; 8 | 9 | export interface CanvasModelListener extends BaseModelListener { 10 | offsetUpdated?(event: BaseEvent & { offsetX: number; offsetY: number }): void; 11 | 12 | zoomUpdated?(event: BaseEvent & { zoom: number }): void; 13 | } 14 | 15 | export class CanvasModel extends BaseModel { 16 | selectedLayer: CanvasLayerModel; 17 | layers: GraphModelOrdered; 18 | 19 | //control variables 20 | offsetX: number; 21 | offsetY: number; 22 | zoom: number; 23 | 24 | constructor() { 25 | super("canvas"); 26 | this.selectedLayer = null; 27 | this.layers = new GraphModelOrdered("layers"); 28 | this.layers.setParentDelegate(this); 29 | this.offsetX = 0; 30 | this.offsetY = 0; 31 | this.zoom = 1; 32 | } 33 | 34 | serialize(): any { 35 | return { 36 | ...super.serialize(), 37 | layers: this.layers.serialize(), 38 | offsetX: this.offsetX, 39 | offsetY: this.offsetY, 40 | zoom: this.zoom 41 | }; 42 | } 43 | 44 | deSerialize(event: DeserializeEvent): void { 45 | super.deSerialize(event); 46 | this.layers.deSerialize(event.subset("layers")); 47 | this.offsetX = event.data["offsetX"]; 48 | this.offsetY = event.data["offsetY"]; 49 | this.zoom = event.data["zoom"]; 50 | } 51 | 52 | getOffsetY() { 53 | return this.offsetY; 54 | } 55 | 56 | getOffsetX() { 57 | return this.offsetX; 58 | } 59 | 60 | getZoomLevel() { 61 | return this.zoom; 62 | } 63 | 64 | setZoomLevel(zoom: number) { 65 | this.zoom = zoom; 66 | this.iterateListeners("zoom changed", (listener: CanvasModelListener, event) => { 67 | if (listener.zoomUpdated) { 68 | listener.zoomUpdated({ ...event, zoom: zoom }); 69 | } 70 | }); 71 | } 72 | 73 | setZoomPercent(percent: number) { 74 | this.setZoomLevel(percent / 100.0); 75 | } 76 | 77 | setOffset(offsetX: number, offsetY: number) { 78 | this.offsetX = offsetX; 79 | this.offsetY = offsetY; 80 | this.iterateListeners("offset changed", (listener: CanvasModelListener, event) => { 81 | if (listener.offsetUpdated) { 82 | listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: offsetY }); 83 | } 84 | }); 85 | } 86 | 87 | removeLayer(layer: CanvasLayerModel) { 88 | this.layers.removeModel(layer); 89 | } 90 | 91 | addLayer(layer: CanvasLayerModel) { 92 | this.layers.addModel(layer); 93 | this.selectedLayer = layer; 94 | } 95 | 96 | getElements(): CanvasElementModel[] { 97 | return _.flatMap(this.layers.getEntities(), layer => { 98 | return _.values(layer.getEntities()); 99 | }); 100 | } 101 | 102 | getSelectedEntities(): CanvasElementModel[] { 103 | return _.filter(this.getElements(), element => { 104 | return element.isSelected(); 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/primitives/ellipse/EllipseElementFactory.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractElementFactory } from "../../AbstractElementFactory"; 2 | import { EllipseElementModel } from "./EllipseElementModel"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import { EllipseElementWidget } from "./EllipseElementWidget"; 5 | import * as React from "react"; 6 | 7 | export class EllipseElementFactory extends AbstractElementFactory { 8 | static NAME = "primitive-circle"; 9 | 10 | constructor() { 11 | super(EllipseElementFactory.NAME); 12 | } 13 | 14 | generateModel(): EllipseElementModel { 15 | return new EllipseElementModel(); 16 | } 17 | 18 | generateWidget(engine: CanvasEngine, model: EllipseElementModel): JSX.Element { 19 | return ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/primitives/ellipse/EllipseElementModel.tsx: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 2 | import { Rectangle } from "../../geometry/Rectangle"; 3 | import { Point } from "../../geometry/Point"; 4 | import * as _ from "lodash"; 5 | import { Polygon } from "../../geometry/Polygon"; 6 | import { DeserializeEvent } from "../../base-models/BaseModel"; 7 | import { EllipseElementFactory } from "./EllipseElementFactory"; 8 | 9 | export class EllipseElementModel extends CanvasElementModel { 10 | radiusX: number; 11 | radiusY: number; 12 | center: Point; 13 | background: string; 14 | 15 | constructor() { 16 | super(EllipseElementFactory.NAME); 17 | this.radiusX = 5; 18 | this.radiusY = 5; 19 | this.center = new Point(0, 0); 20 | this.background = "rgb(0,192,255)"; 21 | } 22 | 23 | static createPointCloud(points: Point[], radius: number = 5) { 24 | return _.map(points, point => { 25 | let model = new EllipseElementModel(); 26 | model.radiusX = radius; 27 | model.radiusY = radius; 28 | model.center = point.clone(); 29 | return model; 30 | }); 31 | } 32 | 33 | static createPointCloudFrom(rectangle: Polygon, radius: number = 5): EllipseElementModel[] { 34 | return EllipseElementModel.createPointCloud(rectangle.getPoints().concat(rectangle.getOrigin()), radius); 35 | } 36 | 37 | getDimensions(): Rectangle { 38 | return new Rectangle( 39 | this.center.x - this.radiusX, 40 | this.center.y - this.radiusY, 41 | this.radiusX * 2, 42 | this.radiusY * 2 43 | ); 44 | } 45 | 46 | deSerialize(event: DeserializeEvent): void { 47 | super.deSerialize(event); 48 | this.radiusX = event.data["radiusX"]; 49 | this.radiusY = event.data["radiusY"]; 50 | this.background = event.data["background"]; 51 | this.center = new Point(event.data["centerX"], event.data["centerY"]); 52 | } 53 | 54 | serialize() { 55 | return { 56 | ...super.serialize(), 57 | radiusX: this.radiusX, 58 | radiusY: this.radiusY, 59 | centerX: this.center.x, 60 | centerY: this.center.y 61 | }; 62 | } 63 | 64 | setDimensions(dimensions: Rectangle) {} 65 | } 66 | -------------------------------------------------------------------------------- /src/primitives/ellipse/EllipseElementWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { EllipseElementModel } from "./EllipseElementModel"; 3 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 4 | 5 | export interface EllipseElementWidgetProps extends BaseWidgetProps { 6 | model: EllipseElementModel; 7 | } 8 | 9 | export interface EllipseElementWidgetState {} 10 | 11 | export class EllipseElementWidget extends BaseWidget { 12 | constructor(props: EllipseElementWidgetProps) { 13 | super("src-ellipsse-element", props); 14 | this.state = {}; 15 | } 16 | 17 | render() { 18 | return ( 19 | 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/primitives/grid/GridElementFactory.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractElementFactory } from "../../AbstractElementFactory"; 2 | import { GridElementModel } from "./GridElementModel"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import { GridElementWidget } from "./GridElementWidget"; 5 | import * as React from "react"; 6 | 7 | export class GridElementFactory extends AbstractElementFactory { 8 | static NAME = "primitive-grid"; 9 | 10 | constructor() { 11 | super(GridElementFactory.NAME); 12 | } 13 | 14 | generateModel(): GridElementModel { 15 | return new GridElementModel(); 16 | } 17 | 18 | generateWidget(engine: CanvasEngine, model: GridElementModel): JSX.Element { 19 | return ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/primitives/grid/GridElementModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 2 | import { Rectangle } from "../../geometry/Rectangle"; 3 | import { DeserializeEvent } from "../../base-models/BaseModel"; 4 | import { GridElementFactory } from "./GridElementFactory"; 5 | 6 | export class GridElementModel extends CanvasElementModel { 7 | sizeX: number; 8 | sizeY: number; 9 | color: string; 10 | thickness: number; 11 | 12 | constructor() { 13 | super(GridElementFactory.NAME); 14 | this.sizeX = 50; 15 | this.sizeY = 50; 16 | this.color = "rgba(0,0,0,0.1)"; 17 | this.thickness = 1; 18 | } 19 | 20 | getDimensions(): Rectangle { 21 | return undefined; 22 | } 23 | 24 | setDimensions(dimensions: Rectangle) {} 25 | 26 | deSerialize(event: DeserializeEvent): void { 27 | super.deSerialize(event); 28 | this.sizeX = event.data["sizeX"]; 29 | this.sizeY = event.data["sizeY"]; 30 | this.color = event.data["color"]; 31 | this.thickness = event.data["thickness"]; 32 | } 33 | 34 | serialize(): { selected: boolean } { 35 | return { 36 | ...super.serialize(), 37 | sizeX: this.sizeX, 38 | sizeY: this.sizeY, 39 | color: this.color, 40 | thickness: this.thickness 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/primitives/grid/GridElementWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { CanvasEngine } from "../../CanvasEngine"; 3 | import { GridElementModel } from "./GridElementModel"; 4 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 5 | 6 | export interface GridElementWidgetProps extends BaseWidgetProps { 7 | engine: CanvasEngine; 8 | model: GridElementModel; 9 | } 10 | 11 | export interface GridElementWidgetState {} 12 | 13 | export class GridElementWidget extends BaseWidget { 14 | constructor(props: GridElementWidgetProps) { 15 | super("src-grid-element", props); 16 | this.state = {}; 17 | } 18 | 19 | render() { 20 | let childrenX = []; 21 | let offsetX = 22 | this.props.engine.getModel().offsetX % 23 | (this.props.model.sizeX * this.props.engine.getModel().getZoomLevel()); 24 | let spacingX = this.props.model.sizeX * this.props.engine.getModel().getZoomLevel(); 25 | let totalChildrenX = this.props.engine.getCanvasWidget().dimension.realDimensions.getWidth() / spacingX; 26 | for (let i = 0; i < totalChildrenX; i++) { 27 | let x = offsetX + spacingX * i; 28 | childrenX.push( 29 | 38 | ); 39 | } 40 | 41 | let childrenY = []; 42 | let offsetY = 43 | this.props.engine.getModel().offsetY % 44 | (this.props.model.sizeY * this.props.engine.getModel().getZoomLevel()); 45 | let spacingY = this.props.model.sizeY * this.props.engine.getModel().getZoomLevel(); 46 | let totalChildrenY = this.props.engine.getCanvasWidget().dimension.realDimensions.getHeight() / spacingY; 47 | for (let i = 0; i < totalChildrenY; i++) { 48 | let y = offsetY + spacingY * i; 49 | childrenY.push( 50 | 59 | ); 60 | } 61 | 62 | return ( 63 | 64 | {childrenX} 65 | {childrenY} 66 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/primitives/paper/PaperElementFactory.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractElementFactory } from "../../AbstractElementFactory"; 2 | import { PaperElementModel } from "./PaperElementModel"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import { PaperElementWidget } from "./PaperElementWidget"; 5 | import * as React from "react"; 6 | 7 | export class PaperElementFactory extends AbstractElementFactory { 8 | static NAME = "primitive-paper"; 9 | 10 | constructor() { 11 | super(PaperElementFactory.NAME); 12 | } 13 | 14 | generateModel(): PaperElementModel { 15 | return new PaperElementModel(); 16 | } 17 | 18 | generateWidget(engine: CanvasEngine, model: PaperElementModel): JSX.Element { 19 | return ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/primitives/paper/PaperElementModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 2 | import { Rectangle } from "../../geometry/Rectangle"; 3 | import { PaperElementFactory } from "./PaperElementFactory"; 4 | 5 | export class PaperElementModel extends CanvasElementModel { 6 | dimensions: Rectangle; 7 | 8 | protected width: number; 9 | protected height: number; 10 | protected dpi: number; 11 | 12 | static INCH = 25.4; //mm 13 | 14 | constructor() { 15 | super(PaperElementFactory.NAME); 16 | this.dimensions = new Rectangle(); 17 | this.setA4(); 18 | } 19 | 20 | setA4() { 21 | this.updateDimensions(210, 297, 300); 22 | } 23 | 24 | updateDimensions(width: number, height: number, dpi: number) { 25 | this.width = width; 26 | this.height = height; 27 | this.dpi = dpi; 28 | this.recomputeDimensions(); 29 | } 30 | 31 | recomputeDimensions() { 32 | this.dimensions = new Rectangle( 33 | 0, 34 | 0, 35 | (this.width * this.dpi) / PaperElementModel.INCH, 36 | (this.height * this.dpi) / PaperElementModel.INCH 37 | ); 38 | } 39 | 40 | getDimensions(): Rectangle { 41 | return this.dimensions; 42 | } 43 | 44 | setDimensions(dimensions: Rectangle) {} 45 | } 46 | -------------------------------------------------------------------------------- /src/primitives/paper/PaperElementWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 3 | import { PaperElementModel } from "./PaperElementModel"; 4 | 5 | export interface PaperElementWidgetProps extends BaseWidgetProps { 6 | model: PaperElementModel; 7 | } 8 | 9 | export class PaperElementWidget extends BaseWidget { 10 | constructor(props) { 11 | super("src-paper", props); 12 | } 13 | 14 | render() { 15 | const dim = this.props.model.getDimensions(); 16 | return ( 17 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/primitives/rectangle/RectangleElementFactory.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractElementFactory } from "../../AbstractElementFactory"; 2 | import { RectangleElementModel } from "./RectangleElementModel"; 3 | import { RectangleElementWidget } from "./RectangleElementWidget"; 4 | import * as React from "react"; 5 | import { CanvasEngine } from "../../CanvasEngine"; 6 | 7 | export class RectangleElementFactory extends AbstractElementFactory { 8 | static NAME = "primitive-rectangle"; 9 | 10 | constructor() { 11 | super(RectangleElementFactory.NAME); 12 | } 13 | 14 | generateModel(): RectangleElementModel { 15 | return new RectangleElementModel(); 16 | } 17 | 18 | generateWidget(engine: CanvasEngine, model: RectangleElementModel): JSX.Element { 19 | return ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/primitives/rectangle/RectangleElementModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 2 | import { Rectangle } from "../../geometry/Rectangle"; 3 | import { DeserializeEvent } from "../../base-models/BaseModel"; 4 | import { RectangleElementFactory } from "./RectangleElementFactory"; 5 | 6 | export class RectangleElementModel extends CanvasElementModel { 7 | border: number; 8 | borderColor: string; 9 | background: string; 10 | dimensions: Rectangle; 11 | 12 | constructor() { 13 | super(RectangleElementFactory.NAME); 14 | this.border = 2; 15 | this.borderColor = "black"; 16 | this.background = "rgb(0,192,255)"; 17 | this.dimensions = new Rectangle(0, 0, 100, 100); 18 | this.selected = false; 19 | } 20 | 21 | serialize() { 22 | return { 23 | ...super.serialize(), 24 | dimensions: this.dimensions.serialize() 25 | }; 26 | } 27 | 28 | deSerialize(event: DeserializeEvent): void { 29 | super.deSerialize(event); 30 | this.dimensions.deserialize(event.data["dimensions"]); 31 | } 32 | 33 | getDimensions(): Rectangle { 34 | return this.dimensions; 35 | } 36 | 37 | setDimensions(dimensions: Rectangle) { 38 | this.dimensions = dimensions; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/primitives/rectangle/RectangleElementWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 3 | import { RectangleElementModel } from "./RectangleElementModel"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | import { PressElementEvent, UnPressElementEvent } from "../../event-bus/events/elements"; 6 | 7 | export interface SquareElementWidgetProps extends BaseWidgetProps { 8 | model: RectangleElementModel; 9 | engine: CanvasEngine; 10 | } 11 | 12 | export interface SquareElementWidgetState {} 13 | 14 | export class RectangleElementWidget extends BaseWidget { 15 | constructor(props: SquareElementWidgetProps) { 16 | super("src-primitive-rectangle", props); 17 | this.state = {}; 18 | } 19 | 20 | render() { 21 | let dimensions = this.props.model.dimensions; 22 | return ( 23 | { 27 | this.props.engine.getEventBus().fireEvent(new PressElementEvent(this, this.props.model)); 28 | }} 29 | onMouseUp={event => { 30 | this.props.engine.getEventBus().fireEvent(new UnPressElementEvent(this, this.props.model)); 31 | }} 32 | /> 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/primitives/selection/SelectionElementFactory.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractElementFactory } from "../../AbstractElementFactory"; 2 | import { SelectionElementModel } from "./SelectionElementModel"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import { SelectionElementWidget } from "./SelectionElementWidget"; 5 | import * as React from "react"; 6 | import { ResizeDimensionsState } from "../../state-machine/states/ResizeDimensionsState"; 7 | import { ResizeOriginDimensionsState } from "../../state-machine/states/ResizeOriginDimensionState"; 8 | import { RotateElementsState } from "../../state-machine/states/RotateElementsState"; 9 | 10 | export class SelectionElementFactory extends AbstractElementFactory { 11 | constructor() { 12 | super("selection"); 13 | } 14 | 15 | generateModel(): SelectionElementModel { 16 | return new SelectionElementModel(); 17 | } 18 | 19 | getCanvasStates() { 20 | return [ 21 | new ResizeOriginDimensionsState(this.engine), 22 | new ResizeDimensionsState(this.engine), 23 | new RotateElementsState(this.engine) 24 | ]; 25 | } 26 | 27 | generateWidget(engine: CanvasEngine, model: SelectionElementModel): JSX.Element { 28 | return ; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/primitives/selection/SelectionElementModel.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 2 | import * as _ from "lodash"; 3 | import { Rectangle } from "../../geometry/Rectangle"; 4 | import { DeserializeEvent } from "../../base-models/BaseModel"; 5 | 6 | export class SelectionElementModel extends CanvasElementModel { 7 | models: CanvasElementModel[]; 8 | 9 | constructor() { 10 | super("selection"); 11 | this.models = []; 12 | } 13 | 14 | setModels(models: CanvasElementModel[]) { 15 | this.models = models; 16 | } 17 | 18 | getModels(): CanvasElementModel[] { 19 | return this.models; 20 | } 21 | 22 | getDimensions(): Rectangle { 23 | return Rectangle.boundingBoxFromPolygons( 24 | _.map(this.models, model => { 25 | return model.getDimensions(); 26 | }) 27 | ); 28 | } 29 | 30 | setDimensions(dimensions: Rectangle) {} 31 | 32 | deSerialize(event: DeserializeEvent): void { 33 | super.deSerialize(event); 34 | this.models = _.map(event.data["models"], modelID => { 35 | return event.cache[modelID]; 36 | }) as any; 37 | } 38 | 39 | serialize() { 40 | return { 41 | ...super.serialize(), 42 | models: _.map(this.models, model => { 43 | return model.getID(); 44 | }) 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/primitives/selection/SelectionElementWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 3 | import { SelectionGroupWidget } from "./SelectionGroupWidget"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | import { SelectionElementModel } from "./SelectionElementModel"; 6 | 7 | export interface SelectionElementWidgetProps extends BaseWidgetProps { 8 | engine: CanvasEngine; 9 | model: SelectionElementModel; 10 | } 11 | 12 | export interface SelectionElementWidgetState {} 13 | 14 | export class SelectionElementWidget extends BaseWidget { 15 | constructor(props: SelectionElementWidgetProps) { 16 | super("src-selection-group", props); 17 | this.state = {}; 18 | } 19 | 20 | render() { 21 | return ; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/primitives/selection/SelectionGroupWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BaseWidget, BaseWidgetProps, MouseWidget } from "@projectstorm/react-core"; 3 | import { AnchorWidget } from "../../widgets/AnchorWidget"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | import { SelectionElementModel } from "./SelectionElementModel"; 6 | import { ModelAnchorInputPosition } from "../../state-machine/input/ModelAnchorInput"; 7 | import { ModelRotateInput } from "../../state-machine/input/ModelRotateInput"; 8 | 9 | export interface SelectionGroupWidgetProps extends BaseWidgetProps { 10 | model: SelectionElementModel; 11 | engine: CanvasEngine; 12 | } 13 | 14 | export interface SelectionGroupWidgetState {} 15 | 16 | export class SelectionGroupWidget extends BaseWidget { 17 | constructor(props: SelectionGroupWidgetProps) { 18 | super("src-selection-group", props); 19 | this.state = {}; 20 | } 21 | 22 | render() { 23 | let dimension = this.props.model.getDimensions().toRealDimensions(this.props.engine.getModel()); 24 | return ( 25 |
34 | { 40 | this.props.engine.getStateMachine().addInput(new ModelRotateInput(this.props.model)); 41 | }} 42 | mouseUpEvent={() => { 43 | this.props.engine.getStateMachine().removeInput(ModelRotateInput.NAME); 44 | }} 45 | /> 46 | 52 | 58 | 64 | 70 | 76 | 82 | 88 | 94 |
95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/sass/_AnchorWidget.scss: -------------------------------------------------------------------------------- 1 | .src-anchor { 2 | background: lightgray; 3 | border: solid 1px black; 4 | position: absolute; 5 | min-width: 8px; 6 | min-height: 8px; 7 | box-sizing: border-box; 8 | cursor: move; 9 | pointer-events: all; 10 | 11 | &:hover { 12 | border: 1px blue solid; 13 | background: cyan; 14 | min-width: 12px; 15 | min-height: 12px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/sass/_CanvasLayerWidget.scss: -------------------------------------------------------------------------------- 1 | .src-canvas-layer { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | transform-origin: 0 0; 6 | overflow: visible !important; 7 | pointer-events: none; 8 | 9 | polygon { 10 | pointer-events: all; 11 | } 12 | rect{ 13 | pointer-events: all; 14 | } 15 | ellipse{ 16 | pointer-events: all; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sass/_CanvasWidget.scss: -------------------------------------------------------------------------------- 1 | .src-canvas{ 2 | border: solid 1px black; 3 | height: 100%; 4 | position: relative; 5 | overflow: hidden; 6 | } 7 | -------------------------------------------------------------------------------- /src/sass/_PaperElementWidget.scss: -------------------------------------------------------------------------------- 1 | .src-paper{ 2 | position: absolute; 3 | borer: solid black; 4 | border-radius: 3px; 5 | box-shadow: 0 0 20px rgba(black,0.5); 6 | background: white; 7 | } -------------------------------------------------------------------------------- /src/sass/_SelectionGroupWidget.scss: -------------------------------------------------------------------------------- 1 | .src-selection-group { 2 | position: absolute; 3 | border: solid 1px rgba(black, 0.3); 4 | box-sizing: border-box; 5 | $amount: 10px; 6 | 7 | &__rotate { 8 | top: - $amount - 15px; 9 | left: 50%; 10 | transform: translateX(-50%); 11 | position: absolute; 12 | border-radius: 5px; 13 | background: greenyellow; 14 | width: 10px; 15 | height: 10px; 16 | border: solid 1px darkgreen; 17 | box-sizing: border-box; 18 | pointer-events: all; 19 | } 20 | 21 | &__top-left { 22 | top: -$amount; 23 | left: -$amount; 24 | } 25 | 26 | &__top { 27 | top: -$amount; 28 | left: 50%; 29 | transform: translateX(-50%); 30 | } 31 | 32 | &__top-right { 33 | top: -$amount; 34 | right: -$amount; 35 | } 36 | 37 | &__left { 38 | left: -$amount; 39 | top: 50%; 40 | transform: translateY(-50%); 41 | } 42 | 43 | &__right { 44 | right: -$amount; 45 | top: 50%; 46 | transform: translateY(-50%); 47 | } 48 | 49 | &__bot-left { 50 | bottom: -$amount; 51 | left: -$amount; 52 | } 53 | 54 | &__bot { 55 | bottom: -$amount; 56 | left: 50%; 57 | transform: translateX(-50%); 58 | } 59 | 60 | &__bot-right { 61 | bottom: -$amount; 62 | right: -$amount; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import "CanvasWidget"; 2 | @import "CanvasLayerWidget"; 3 | @import "SelectionGroupWidget"; 4 | @import "AnchorWidget"; 5 | @import "PaperElementWidget"; 6 | -------------------------------------------------------------------------------- /src/state-machine/AbstractDisplacementState.ts: -------------------------------------------------------------------------------- 1 | import { AbstractState } from "./AbstractState"; 2 | import { StateMachine } from "./StateMachine"; 3 | import { CanvasEngine } from "../CanvasEngine"; 4 | import { MouseDownInput } from "./input/MouseDownInput"; 5 | import { InlineAction } from "../event-bus/InlineAction"; 6 | import { MouseMoveEvent } from "../event-bus/events/mouse"; 7 | 8 | export abstract class AbstractDisplacementState extends AbstractState { 9 | initialMouse: MouseDownInput; 10 | 11 | constructor(name: string, engine: CanvasEngine) { 12 | super(name, engine); 13 | this.requireInput(MouseDownInput.NAME); 14 | this.registerAction( 15 | new InlineAction(MouseMoveEvent.NAME, event => { 16 | if (this.initialMouse) { 17 | this.processDisplacement( 18 | event.mouseX - this.initialMouse.mouseX, 19 | event.mouseY - this.initialMouse.mouseY 20 | ); 21 | } 22 | }) 23 | ); 24 | } 25 | 26 | abstract processDisplacement(displacementX, displacementY); 27 | 28 | activated(machine: StateMachine) { 29 | super.activated(machine); 30 | this.initialMouse = machine.getInput(MouseDownInput.NAME) as MouseDownInput; 31 | } 32 | 33 | deactivated(machine: StateMachine) { 34 | super.deactivated(machine); 35 | this.initialMouse = null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/state-machine/AbstractState.ts: -------------------------------------------------------------------------------- 1 | import { StateMachine } from "./StateMachine"; 2 | import { Action } from "../event-bus/Action"; 3 | import { CanvasEngine } from "../CanvasEngine"; 4 | import * as _ from "lodash"; 5 | 6 | export abstract class AbstractState { 7 | engine: CanvasEngine; 8 | name: string; 9 | requiredInputs: string[]; 10 | actions: Action[]; 11 | 12 | constructor(name: string, engine: CanvasEngine) { 13 | this.engine = engine; 14 | this.name = name; 15 | this.requiredInputs = []; 16 | this.actions = []; 17 | } 18 | 19 | registerAction(action: Action) { 20 | this.actions.push(action); 21 | } 22 | 23 | requireInput(input: string) { 24 | this.requiredInputs.push(input); 25 | } 26 | 27 | getName() { 28 | return this.name; 29 | } 30 | 31 | shouldStateActivate(machine: StateMachine): boolean { 32 | let keys = _.keys(machine.inputs); 33 | if (_.intersection(keys, this.requiredInputs).length === this.requiredInputs.length) { 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | 40 | activated(machine: StateMachine) { 41 | _.forEach(this.actions, action => { 42 | this.engine.getEventBus().registerAction(action); 43 | }); 44 | } 45 | 46 | deactivated(machine: StateMachine) { 47 | _.forEach(this.actions, action => { 48 | this.engine.getEventBus().unRegisterAction(action); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/state-machine/AbstractStateMachineInput.ts: -------------------------------------------------------------------------------- 1 | export class AbstractStateMachineInput { 2 | name: string; 3 | 4 | constructor(name: string) { 5 | this.name = name; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/state-machine/StateMachine.ts: -------------------------------------------------------------------------------- 1 | import { AbstractState } from "./AbstractState"; 2 | import * as _ from "lodash"; 3 | import { AbstractStateMachineInput } from "./AbstractStateMachineInput"; 4 | import { BaseEvent, BaseListener, BaseObject } from "@projectstorm/react-core"; 5 | 6 | export interface StateMachineListener extends BaseListener { 7 | stateChanged(event: BaseEvent & { state: AbstractState }); 8 | } 9 | 10 | export class StateMachine extends BaseObject { 11 | inputs: { [name: string]: AbstractStateMachineInput }; 12 | states: { [name: string]: AbstractState }; 13 | state: AbstractState; 14 | 15 | constructor() { 16 | super(); 17 | this.inputs = {}; 18 | this.states = {}; 19 | this.state = null; 20 | } 21 | 22 | addState(state: AbstractState) { 23 | if (this.states[state.getName()]) { 24 | throw "A state with name: " + state.getName() + " is already registered"; 25 | } 26 | this.states[state.getName()] = state; 27 | } 28 | 29 | removeInput(type: string, fire: boolean = true) { 30 | if (!this.inputs[type]) { 31 | return; 32 | } 33 | 34 | delete this.inputs[type]; 35 | if (fire) { 36 | this.process(); 37 | } 38 | } 39 | 40 | addInput(input: AbstractStateMachineInput, fire: boolean = true): AbstractStateMachineInput { 41 | this.inputs[input.name] = input; 42 | if (fire) { 43 | this.process(); 44 | } 45 | return input; 46 | } 47 | 48 | getInput(name: string): AbstractStateMachineInput { 49 | return _.find(this.inputs, { name: name }); 50 | } 51 | 52 | clearState() { 53 | if (this.state) { 54 | this.state.deactivated(this); 55 | } else { 56 | return; 57 | } 58 | this.state = null; 59 | this.fireStateChanged(); 60 | } 61 | 62 | fireStateChanged() { 63 | this.iterateListeners("state changed", (listener, event) => { 64 | if (listener.stateChanged) { 65 | listener.stateChanged({ ...event, state: this.state }); 66 | } 67 | }); 68 | } 69 | 70 | setState(state: AbstractState) { 71 | // deactivate previous state 72 | if (this.state && state) { 73 | if (this.state.name !== state.name) { 74 | this.state.deactivated(this); 75 | this.state = state; 76 | state.activated(this); 77 | this.fireStateChanged(); 78 | } 79 | } else { 80 | // there never was a state 81 | this.state = state; 82 | state.activated(this); 83 | this.fireStateChanged(); 84 | } 85 | } 86 | 87 | process() { 88 | // check for possible reactions to current inputs 89 | let possibleReactions = _.map( 90 | _.filter(this.states, state => { 91 | return state.shouldStateActivate(this); 92 | }), 93 | state => { 94 | return state.getName(); 95 | } 96 | ); 97 | 98 | if (possibleReactions.length === 0) { 99 | this.clearState(); 100 | } else if (possibleReactions.length > 0) { 101 | this.setState(this.states[possibleReactions[0]]); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/state-machine/input/KeyInput.ts: -------------------------------------------------------------------------------- 1 | import { AbstractStateMachineInput } from "../AbstractStateMachineInput"; 2 | import { EventBus } from "../../event-bus/EventBus"; 3 | import { InlineAction } from "../../event-bus/InlineAction"; 4 | import { KeyDownEvent, KeyUpEvent } from "../../event-bus/events/key"; 5 | import { StateMachine } from "../StateMachine"; 6 | 7 | export enum KeyCode { 8 | SHIFT = "Shift", 9 | CONTROL = "Control" 10 | } 11 | 12 | export class KeyInput extends AbstractStateMachineInput { 13 | key: any; 14 | 15 | static identifier(key: string) { 16 | return "key-" + key; 17 | } 18 | 19 | constructor(key: string) { 20 | super(KeyInput.identifier(key)); 21 | this.key = key; 22 | } 23 | 24 | static installActions(machine: StateMachine, eventBus: EventBus) { 25 | eventBus.registerAction( 26 | new InlineAction(KeyDownEvent.NAME, event => { 27 | machine.addInput(new KeyInput(event.key)); 28 | }) 29 | ); 30 | eventBus.registerAction( 31 | new InlineAction(KeyUpEvent.NAME, event => { 32 | machine.removeInput(KeyInput.identifier(event.key)); 33 | }) 34 | ); 35 | } 36 | 37 | isShift(): boolean { 38 | return this.key === KeyCode.SHIFT; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/state-machine/input/ModelAnchorInput.ts: -------------------------------------------------------------------------------- 1 | import { SelectionElementModel } from "../../primitives/selection/SelectionElementModel"; 2 | import { AbstractStateMachineInput } from "../AbstractStateMachineInput"; 3 | 4 | export enum ModelAnchorInputPosition { 5 | TOP, 6 | TOP_LEFT, 7 | TOP_RIGHT, 8 | LEFT, 9 | RIGHT, 10 | BOT, 11 | BOT_LEFT, 12 | BOT_RIGHT 13 | } 14 | 15 | export class ModelAnchorInput extends AbstractStateMachineInput { 16 | selectionModel: SelectionElementModel; 17 | anchor: ModelAnchorInputPosition; 18 | 19 | static NAME = "model-anchor"; 20 | 21 | constructor(model: SelectionElementModel, anchor: ModelAnchorInputPosition) { 22 | super(ModelAnchorInput.NAME); 23 | this.selectionModel = model; 24 | this.anchor = anchor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/state-machine/input/ModelElementInput.ts: -------------------------------------------------------------------------------- 1 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 2 | import { AbstractStateMachineInput } from "../AbstractStateMachineInput"; 3 | import { EventBus } from "../../event-bus/EventBus"; 4 | import { InlineAction } from "../../event-bus/InlineAction"; 5 | import { StateMachine } from "../StateMachine"; 6 | import { PressElementEvent, UnPressElementEvent } from "../../event-bus/events/elements"; 7 | 8 | export class ModelElementInput extends AbstractStateMachineInput { 9 | element: CanvasElementModel; 10 | 11 | static NAME = "model-element"; 12 | 13 | constructor(element: CanvasElementModel) { 14 | super(ModelElementInput.NAME); 15 | this.element = element; 16 | } 17 | 18 | static installActions(machine: StateMachine, eventBus: EventBus) { 19 | eventBus.registerAction( 20 | new InlineAction(PressElementEvent.NAME, event => { 21 | machine.addInput(new ModelElementInput(event.element)); 22 | }) 23 | ); 24 | eventBus.registerAction( 25 | new InlineAction(UnPressElementEvent.NAME, event => { 26 | machine.removeInput(ModelElementInput.NAME); 27 | }) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/state-machine/input/ModelRotateInput.ts: -------------------------------------------------------------------------------- 1 | import { SelectionElementModel } from "../../primitives/selection/SelectionElementModel"; 2 | import { AbstractStateMachineInput } from "../AbstractStateMachineInput"; 3 | 4 | export class ModelRotateInput extends AbstractStateMachineInput { 5 | selectionModel: SelectionElementModel; 6 | 7 | static NAME = "model-rotate"; 8 | 9 | constructor(model: SelectionElementModel) { 10 | super(ModelRotateInput.NAME); 11 | this.selectionModel = model; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/state-machine/input/MouseDownInput.ts: -------------------------------------------------------------------------------- 1 | import { AbstractStateMachineInput } from "../AbstractStateMachineInput"; 2 | import { EventBus } from "../../event-bus/EventBus"; 3 | import { InlineAction } from "../../event-bus/InlineAction"; 4 | import { StateMachine } from "../StateMachine"; 5 | import { MouseDownEvent, MouseUpEvent } from "../../event-bus/events/mouse"; 6 | 7 | export class MouseDownInput extends AbstractStateMachineInput { 8 | mouseX: number; 9 | mouseY: number; 10 | originalEvent: MouseDownEvent; 11 | 12 | static NAME = "mouse-down"; 13 | 14 | constructor(event: MouseDownEvent) { 15 | super(MouseDownInput.NAME); 16 | this.mouseX = event.mouseX; 17 | this.mouseY = event.mouseY; 18 | this.originalEvent = event; 19 | } 20 | 21 | static installActions(machine: StateMachine, eventBus: EventBus) { 22 | eventBus.registerAction( 23 | new InlineAction(MouseDownEvent.NAME, event => { 24 | machine.addInput(new MouseDownInput(event)); 25 | }) 26 | ); 27 | eventBus.registerAction( 28 | new InlineAction(MouseUpEvent.NAME, event => { 29 | machine.removeInput(MouseDownInput.NAME); 30 | }) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/state-machine/states/DefaultState.ts: -------------------------------------------------------------------------------- 1 | import { AbstractState } from "../AbstractState"; 2 | import { CanvasEngine } from "../../CanvasEngine"; 3 | import { SelectElementAction } from "../../event-bus/actions/SelectElementAction"; 4 | 5 | export class DefaultState extends AbstractState { 6 | constructor(engine: CanvasEngine) { 7 | super("default-state", engine); 8 | this.registerAction(new SelectElementAction(engine, false)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/state-machine/states/ResizeDimensionsState.ts: -------------------------------------------------------------------------------- 1 | import { StateMachine } from "../StateMachine"; 2 | import * as _ from "lodash"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import { Rectangle } from "../../geometry/Rectangle"; 5 | import { Point } from "../../geometry/Point"; 6 | import { AbstractDisplacementState } from "../AbstractDisplacementState"; 7 | import { Matrix } from "mathjs"; 8 | import { ModelAnchorInput, ModelAnchorInputPosition } from "../input/ModelAnchorInput"; 9 | 10 | export class ResizeDimensionsState extends AbstractDisplacementState { 11 | anchorInput: ModelAnchorInput; 12 | initialDimensions: Rectangle[]; 13 | initialDimension: Rectangle; 14 | 15 | constructor(engine: CanvasEngine) { 16 | super("resize-dimension", engine); 17 | this.requireInput(ModelAnchorInput.NAME); 18 | this.engine = engine; 19 | } 20 | 21 | activated(machine: StateMachine) { 22 | super.activated(machine); 23 | // get the input handles 24 | this.anchorInput = machine.getInput(ModelAnchorInput.NAME) as ModelAnchorInput; 25 | 26 | // store the initial dimensions 27 | this.initialDimension = this.anchorInput.selectionModel.getDimensions().clone(); 28 | this.initialDimensions = _.map(this.anchorInput.selectionModel.getModels(), model => { 29 | return model.getDimensions(); 30 | }); 31 | } 32 | 33 | processDisplacement(displacementX, displacementY) { 34 | const zoom = this.engine.getModel().getZoomLevel(); 35 | 36 | // work out the distance difference 37 | const distanceX = displacementX / zoom; 38 | const distanceY = displacementY / zoom; 39 | 40 | // work out the scaling factors for both positive and negative cases 41 | const scaleX = (this.initialDimension.getWidth() + distanceX) / this.initialDimension.getWidth(); 42 | const scaleY = (this.initialDimension.getHeight() + distanceY) / this.initialDimension.getHeight(); 43 | const scaleX2 = (this.initialDimension.getWidth() - distanceX) / this.initialDimension.getWidth(); 44 | const scaleY2 = (this.initialDimension.getHeight() - distanceY) / this.initialDimension.getHeight(); 45 | 46 | // construct the correct transform matrix 47 | let transform: Matrix = null; 48 | if (this.anchorInput.anchor === ModelAnchorInputPosition.TOP_LEFT) { 49 | transform = Point.createScaleMatrix(scaleX2, scaleY2, this.initialDimension.getBottomRight()); 50 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.TOP) { 51 | transform = Point.createScaleMatrix(1, scaleY2, this.initialDimension.getBottomMiddle()); 52 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.TOP_RIGHT) { 53 | transform = Point.createScaleMatrix(scaleX, scaleY2, this.initialDimension.getBottomLeft()); 54 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.RIGHT) { 55 | transform = Point.createScaleMatrix(scaleX, 1, this.initialDimension.getLeftMiddle()); 56 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.BOT_RIGHT) { 57 | transform = Point.createScaleMatrix(scaleX, scaleY, this.initialDimension.getTopLeft()); 58 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.BOT) { 59 | transform = Point.createScaleMatrix(1, scaleY, this.initialDimension.getTopMiddle()); 60 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.BOT_LEFT) { 61 | transform = Point.createScaleMatrix(scaleX2, scaleY, this.initialDimension.getTopRight()); 62 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.LEFT) { 63 | transform = Point.createScaleMatrix(scaleX2, 1, this.initialDimension.getRightMiddle()); 64 | } 65 | 66 | _.forEach(this.anchorInput.selectionModel.getModels(), (model, index) => { 67 | let dimensions = this.initialDimensions[index].clone(); 68 | dimensions.transform(transform); 69 | model.setDimensions(dimensions); 70 | }); 71 | 72 | this.engine.repaint(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/state-machine/states/ResizeOriginDimensionState.ts: -------------------------------------------------------------------------------- 1 | import { StateMachine } from "../StateMachine"; 2 | import * as _ from "lodash"; 3 | import { CanvasEngine } from "../../CanvasEngine"; 4 | import { Rectangle } from "../../geometry/Rectangle"; 5 | import { Point } from "../../geometry/Point"; 6 | import { AbstractDisplacementState } from "../AbstractDisplacementState"; 7 | import { Matrix } from "mathjs"; 8 | import { ModelAnchorInput, ModelAnchorInputPosition } from "../input/ModelAnchorInput"; 9 | import { KeyCode, KeyInput } from "../input/KeyInput"; 10 | 11 | export class ResizeOriginDimensionsState extends AbstractDisplacementState { 12 | anchorInput: ModelAnchorInput; 13 | initialDimensions: Rectangle[]; 14 | initialDimension: Rectangle; 15 | 16 | constructor(engine: CanvasEngine) { 17 | super("resize-origin-dimension", engine); 18 | this.requireInput(ModelAnchorInput.NAME); 19 | this.requireInput(KeyInput.identifier(KeyCode.SHIFT)); 20 | this.engine = engine; 21 | } 22 | 23 | activated(machine: StateMachine) { 24 | super.activated(machine); 25 | // get the input handles 26 | this.anchorInput = machine.getInput(ModelAnchorInput.NAME) as ModelAnchorInput; 27 | 28 | // store the initial dimensions 29 | this.initialDimension = this.anchorInput.selectionModel.getDimensions().clone(); 30 | this.initialDimensions = _.map(this.anchorInput.selectionModel.getModels(), model => { 31 | return model.getDimensions(); 32 | }); 33 | } 34 | 35 | processDisplacement(displacementX, displacementY) { 36 | const zoom = this.engine.getModel().getZoomLevel(); 37 | 38 | // work out the distance difference 39 | const distanceX = displacementX / zoom; 40 | const distanceY = displacementY / zoom; 41 | 42 | // work out the scaling factors for both positive and negative cases 43 | const scaleX = ((this.initialDimension.getWidth() + distanceX) * 2) / this.initialDimension.getWidth(); 44 | const scaleY = ((this.initialDimension.getHeight() + distanceY) * 2) / this.initialDimension.getHeight(); 45 | const scaleX2 = ((this.initialDimension.getWidth() - distanceX) * 2) / this.initialDimension.getWidth(); 46 | const scaleY2 = ((this.initialDimension.getHeight() - distanceY) * 2) / this.initialDimension.getHeight(); 47 | 48 | // construct the correct transform matrix 49 | let transform: Matrix = null; 50 | if (this.anchorInput.anchor === ModelAnchorInputPosition.TOP_LEFT) { 51 | transform = Point.createScaleMatrix(scaleX2, scaleY2, this.initialDimension.getOrigin()); 52 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.TOP) { 53 | transform = Point.createScaleMatrix(1, scaleY2, this.initialDimension.getOrigin()); 54 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.TOP_RIGHT) { 55 | transform = Point.createScaleMatrix(scaleX, scaleY2, this.initialDimension.getOrigin()); 56 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.RIGHT) { 57 | transform = Point.createScaleMatrix(scaleX, 1, this.initialDimension.getOrigin()); 58 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.BOT_RIGHT) { 59 | transform = Point.createScaleMatrix(scaleX, scaleY, this.initialDimension.getOrigin()); 60 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.BOT) { 61 | transform = Point.createScaleMatrix(1, scaleY, this.initialDimension.getOrigin()); 62 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.BOT_LEFT) { 63 | transform = Point.createScaleMatrix(scaleX2, scaleY, this.initialDimension.getOrigin()); 64 | } else if (this.anchorInput.anchor === ModelAnchorInputPosition.LEFT) { 65 | transform = Point.createScaleMatrix(scaleX2, 1, this.initialDimension.getOrigin()); 66 | } 67 | 68 | _.forEach(this.anchorInput.selectionModel.getModels(), (model, index) => { 69 | let dimensions = this.initialDimensions[index].clone(); 70 | dimensions.transform(transform); 71 | model.setDimensions(dimensions); 72 | }); 73 | 74 | this.engine.repaint(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/state-machine/states/RotateElementsState.ts: -------------------------------------------------------------------------------- 1 | import { AbstractState } from "../AbstractState"; 2 | import { CanvasEngine } from "../../CanvasEngine"; 3 | import { InlineAction } from "../../event-bus/InlineAction"; 4 | import { StateMachine } from "../StateMachine"; 5 | import { MouseDownInput } from "../input/MouseDownInput"; 6 | import { MouseMoveEvent } from "../../event-bus/events/mouse"; 7 | import { ModelRotateInput } from "../input/ModelRotateInput"; 8 | import { Point } from "../../geometry/Point"; 9 | import * as _ from "lodash"; 10 | import { Rectangle } from "../../geometry/Rectangle"; 11 | 12 | export class RotateElementsState extends AbstractState { 13 | initialMouse: MouseDownInput; 14 | modelRotateInput: ModelRotateInput; 15 | initialOrigin: Point; 16 | initialDimensions: Rectangle[]; 17 | 18 | constructor(engine: CanvasEngine) { 19 | super("rotate-elements", engine); 20 | this.requireInput(ModelRotateInput.NAME); 21 | this.requireInput(MouseDownInput.NAME); 22 | this.registerAction( 23 | new InlineAction(MouseMoveEvent.NAME, event => { 24 | if (this.initialMouse) { 25 | let degrees = 26 | Math.atan2( 27 | event.getCanvasCoordinates(this.engine).x - 28 | this.initialMouse.originalEvent.getCanvasCoordinates(this.engine).x, 29 | this.initialOrigin.y - event.getCanvasCoordinates(this.engine).y 30 | ) * 31 | (180 / Math.PI); 32 | 33 | if (degrees < 0) { 34 | degrees = 360.0 + degrees; 35 | } 36 | 37 | let transform = Point.createRotateMatrix(degrees / (180 / Math.PI), this.initialOrigin); 38 | 39 | _.forEach(this.modelRotateInput.selectionModel.getModels(), (model, index) => { 40 | let dimensions = this.initialDimensions[index].clone(); 41 | dimensions.transform(transform); 42 | model.setDimensions(dimensions); 43 | }); 44 | 45 | this.engine.repaint(); 46 | } 47 | }) 48 | ); 49 | } 50 | 51 | activated(machine: StateMachine) { 52 | super.activated(machine); 53 | 54 | this.initialMouse = machine.getInput(MouseDownInput.NAME) as MouseDownInput; 55 | this.modelRotateInput = machine.getInput(ModelRotateInput.NAME) as ModelRotateInput; 56 | this.initialOrigin = this.modelRotateInput.selectionModel 57 | .getDimensions() 58 | .getOrigin() 59 | .clone(); 60 | this.initialDimensions = _.map(this.modelRotateInput.selectionModel.getModels(), model => { 61 | return model.getDimensions(); 62 | }); 63 | } 64 | 65 | deactivated(machine: StateMachine) { 66 | super.deactivated(machine); 67 | this.initialMouse = null; 68 | this.modelRotateInput = null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/state-machine/states/SelectElementsState.ts: -------------------------------------------------------------------------------- 1 | import { AbstractState } from "../AbstractState"; 2 | import { CanvasEngine } from "../../CanvasEngine"; 3 | import { KeyCode, KeyInput } from "../input/KeyInput"; 4 | import { SelectElementAction } from "../../event-bus/actions/SelectElementAction"; 5 | 6 | export class SelectElementsState extends AbstractState { 7 | constructor(engine: CanvasEngine) { 8 | super("select-elements", engine); 9 | this.requireInput(KeyInput.identifier(KeyCode.SHIFT)); 10 | this.registerAction(new SelectElementAction(engine, true)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/state-machine/states/TranslateCanvasState.ts: -------------------------------------------------------------------------------- 1 | import { StateMachine } from "../StateMachine"; 2 | import { CanvasEngine } from "../../CanvasEngine"; 3 | import { AbstractDisplacementState } from "../AbstractDisplacementState"; 4 | import { SelectCanvasAction } from "../../event-bus/actions/SelectCanvasAction"; 5 | 6 | export class TranslateCanvasState extends AbstractDisplacementState { 7 | initialOffsetX: number; 8 | initialOffsetY: number; 9 | 10 | constructor(engine: CanvasEngine) { 11 | super("translate-canvas", engine); 12 | this.registerAction(new SelectCanvasAction(engine)); 13 | } 14 | 15 | activated(machine: StateMachine) { 16 | super.activated(machine); 17 | this.initialOffsetX = this.engine.getModel().getOffsetX(); 18 | this.initialOffsetY = this.engine.getModel().getOffsetY(); 19 | } 20 | 21 | processDisplacement(displacementX, displacementY) { 22 | this.engine.getModel().setOffset(this.initialOffsetX + displacementX, this.initialOffsetY + displacementY); 23 | this.engine.repaint(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/state-machine/states/TranslateElementState.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { AbstractDisplacementState } from "../AbstractDisplacementState"; 3 | import { StateMachine } from "../StateMachine"; 4 | import { CanvasEngine } from "../../CanvasEngine"; 5 | import { Rectangle } from "../../geometry/Rectangle"; 6 | import { ModelElementInput } from "../input/ModelElementInput"; 7 | import { CanvasElementModel } from "../../models-canvas/CanvasElementModel"; 8 | 9 | export class TranslateElementState extends AbstractDisplacementState { 10 | initialPosition: { [id: string]: Rectangle }; 11 | initialEntities: { [id: string]: CanvasElementModel }; 12 | 13 | constructor(engine: CanvasEngine) { 14 | super("translate-element", engine); 15 | this.requireInput(ModelElementInput.NAME); 16 | } 17 | 18 | activated(machine: StateMachine) { 19 | super.activated(machine); 20 | this.initialPosition = {}; 21 | this.initialEntities = {}; 22 | let selected = this.engine.getModel().getSelectedEntities(); 23 | _.forEach(selected, selected => { 24 | this.initialEntities[selected.getID()] = selected; 25 | this.initialPosition[selected.getID()] = selected.getDimensions().clone(); 26 | }); 27 | } 28 | 29 | processDisplacement(displacementX, displacementY) { 30 | 31 | const zoom = this.engine.getModel().getZoomLevel(); 32 | 33 | // work out the distance difference 34 | const distanceX = displacementX / zoom; 35 | const distanceY = displacementY / zoom; 36 | 37 | _.forEach(this.initialPosition, (initialPosition, index) => { 38 | const dim = initialPosition.clone(); 39 | dim.translate(distanceX, distanceY); 40 | this.initialEntities[index].setDimensions(dim); 41 | }); 42 | this.engine.repaint(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/tracking/DimensionTracker.ts: -------------------------------------------------------------------------------- 1 | import { CanvasEngine } from "../CanvasEngine"; 2 | import { Rectangle } from "../geometry/Rectangle"; 3 | import { BaseEvent, BaseListener, BaseObject } from "@projectstorm/react-core"; 4 | 5 | export interface DimensionTrackerListener extends BaseListener { 6 | updated(event: BaseEvent); 7 | } 8 | 9 | export class DimensionTracker extends BaseObject { 10 | realDimensions: Rectangle; 11 | enableTracking: boolean; 12 | 13 | constructor() { 14 | super(); 15 | this.enableTracking = true; 16 | this.realDimensions = new Rectangle(); 17 | } 18 | 19 | recompute(canvasEngine: CanvasEngine, clientRect: ClientRect) { 20 | this.realDimensions.updateDimensions(clientRect.left, clientRect.top, clientRect.width, clientRect.height); 21 | } 22 | 23 | updateDimensions(canvasEngine: CanvasEngine, ClientRect: ClientRect) { 24 | if (!this.enableTracking) { 25 | return false; 26 | } 27 | 28 | // store the real dimensions 29 | this.recompute(canvasEngine, ClientRect); 30 | 31 | // fire the update event 32 | this.iterateListeners("dimensions updated", (listener, event) => { 33 | if (listener.updated) { 34 | listener.updated(event); 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tracking/DimensionTrackerWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 3 | import { DimensionTracker } from "./DimensionTracker"; 4 | import { CanvasEngine } from "../CanvasEngine"; 5 | 6 | export interface DimensionTrackerWidgetProps extends BaseWidgetProps { 7 | dimensionTracker: DimensionTracker; 8 | engine: CanvasEngine; 9 | reference: { current: HTMLElement }; 10 | } 11 | 12 | export interface DimensionTrackerWidgetState {} 13 | 14 | export class DimensionTrackerWidget extends BaseWidget { 15 | observer: any; 16 | 17 | constructor(props: DimensionTrackerWidgetProps) { 18 | super("src-dimension-tracker", props); 19 | this.state = {}; 20 | } 21 | 22 | updateDimensions() { 23 | if (this.props.reference.current) { 24 | this.props.dimensionTracker.updateDimensions( 25 | this.props.engine, 26 | this.props.reference.current.getBoundingClientRect() 27 | ); 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | //if resize observer is present, rather use that 33 | if (window["ResizeObserver"]) { 34 | this.observer = new window["ResizeObserver"](entries => { 35 | this.updateDimensions(); 36 | }); 37 | this.observer.observe(this.props.reference.current); 38 | } 39 | this.updateDimensions(); 40 | } 41 | 42 | componentDidUpdate() { 43 | if (!this.observer) { 44 | this.updateDimensions(); 45 | } 46 | } 47 | 48 | render() { 49 | return this.props.children; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/tracking/VirtualDimensionTracker.ts: -------------------------------------------------------------------------------- 1 | import { DimensionTracker } from "./DimensionTracker"; 2 | import { CanvasEngine } from "../CanvasEngine"; 3 | import { Rectangle } from "../geometry/Rectangle"; 4 | 5 | export class VirtualDimensionTracker extends DimensionTracker { 6 | virtualDimensions: Rectangle; 7 | 8 | constructor() { 9 | super(); 10 | this.virtualDimensions = new Rectangle(); 11 | } 12 | 13 | recompute(engine: CanvasEngine, clientRect: ClientRect) { 14 | super.recompute(engine, clientRect); 15 | 16 | let model = engine.getModel(); 17 | let canDimensions = engine.getCanvasWidget().dimension.realDimensions; 18 | // store the virtual dimensions 19 | let zoomLevel = model.getZoomLevel(); 20 | this.virtualDimensions.updateDimensions( 21 | (clientRect.left - canDimensions.getTopLeft().x - model.getOffsetX()) / zoomLevel, 22 | (clientRect.top - canDimensions.getTopLeft().y - model.getOffsetY()) / zoomLevel, 23 | clientRect.width / zoomLevel, 24 | clientRect.height / zoomLevel 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/widgets/AnchorWidget.tsx: -------------------------------------------------------------------------------- 1 | import { BaseWidget, BaseWidgetProps, MouseWidget } from "@projectstorm/react-core"; 2 | import * as React from "react"; 3 | import { CanvasEngine } from "../CanvasEngine"; 4 | import { SelectionElementModel } from "../primitives/selection/SelectionElementModel"; 5 | import { ModelAnchorInput, ModelAnchorInputPosition } from "../state-machine/input/ModelAnchorInput"; 6 | 7 | export interface AnchorWidgetProps extends BaseWidgetProps { 8 | engine: CanvasEngine; 9 | selectionModel: SelectionElementModel; 10 | pos: ModelAnchorInputPosition; 11 | } 12 | 13 | export class AnchorWidget extends BaseWidget { 14 | constructor(props) { 15 | super("src-anchor", props); 16 | } 17 | 18 | componentWillUnmount() { 19 | this.props.engine.getStateMachine().removeInput(ModelAnchorInput.NAME); 20 | } 21 | 22 | render() { 23 | return ( 24 | { 27 | this.props.engine 28 | .getStateMachine() 29 | .addInput(new ModelAnchorInput(this.props.selectionModel, this.props.pos)); 30 | }} 31 | mouseUpEvent={() => { 32 | this.props.engine.getStateMachine().removeInput(ModelAnchorInput.NAME); 33 | }} 34 | extraProps={{ ...this.getProps() }} 35 | /> 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/widgets/CanvasLayerWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { CanvasEngine } from "../CanvasEngine"; 3 | import { CanvasLayerModel } from "../models-canvas/CanvasLayerModel"; 4 | import * as _ from "lodash"; 5 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 6 | 7 | export interface CanvasLayerWidgetProps extends BaseWidgetProps { 8 | engine: CanvasEngine; 9 | layer: CanvasLayerModel; 10 | } 11 | 12 | export interface CanvasLayerWidgetState {} 13 | 14 | export class CanvasLayerWidget extends BaseWidget { 15 | constructor(props: CanvasLayerWidgetProps) { 16 | super("src-canvas-layer", props); 17 | this.state = {}; 18 | } 19 | 20 | getProps() { 21 | let canvas = this.props.engine.getModel(); 22 | let props = super.getProps(); 23 | 24 | // do we apply 25 | if (this.props.layer.isTransformable()) { 26 | props["style"] = { 27 | ...props["style"], 28 | transform: 29 | "translate(" + 30 | canvas.getOffsetX() + 31 | "px," + 32 | canvas.getOffsetY() + 33 | "px) scale(" + 34 | canvas.getZoomLevel() + 35 | ")" 36 | }; 37 | } 38 | 39 | return props; 40 | } 41 | 42 | getChildren() { 43 | return _.map(this.props.layer.getAllEntities(), element => { 44 | return React.cloneElement( 45 | this.props.engine.getFactoryForElement(element).generateWidget(this.props.engine, element), 46 | { key: element.getID() } 47 | ); 48 | }); 49 | } 50 | 51 | render() { 52 | // it might be an SVG layer 53 | if (this.props.layer.isSVG()) { 54 | return {this.getChildren()}; 55 | } 56 | return
{this.getChildren()}
; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/widgets/CanvasWidget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-core"; 3 | import { CanvasEngine } from "../CanvasEngine"; 4 | import * as _ from "lodash"; 5 | import { DimensionTrackerWidget } from "../tracking/DimensionTrackerWidget"; 6 | import { DimensionTracker } from "../tracking/DimensionTracker"; 7 | import { Rectangle } from "../geometry/Rectangle"; 8 | import { KeyDownEvent, KeyUpEvent } from "../event-bus/events/key"; 9 | import { MouseDownEvent, MouseMoveEvent, MouseUpEvent, MouseWheelEvent } from "../event-bus/events/mouse"; 10 | 11 | export interface CanvasWidgetProps extends BaseWidgetProps { 12 | engine: CanvasEngine; 13 | inverseZoom?: boolean; 14 | } 15 | 16 | export interface CanvasWidgetState {} 17 | 18 | export class CanvasWidget extends BaseWidget { 19 | dimension: DimensionTracker; 20 | ref: { current: HTMLElement }; 21 | 22 | // handles 23 | onKeyDownHandle: (event: any) => any; 24 | onKeyUpHandle: (event: any) => any; 25 | onMouseMoveHandle: (event: any) => any; 26 | onMouseDownHandle: (event: any) => any; 27 | onMouseUpHandle: (event: any) => any; 28 | onMouseWheelHandle: (event: WheelEvent) => any; 29 | 30 | constructor(props: CanvasWidgetProps) { 31 | super("src-canvas", props); 32 | this.state = {}; 33 | this.dimension = new DimensionTracker(); 34 | 35 | this.ref = (React as any).createRef(); 36 | 37 | this.onKeyDownHandle = (event: any) => { 38 | this.props.engine.getEventBus().fireEvent(new KeyDownEvent(this, event.key)); 39 | }; 40 | 41 | this.onKeyUpHandle = (event: any) => { 42 | this.props.engine.getEventBus().fireEvent(new KeyUpEvent(this, event.key)); 43 | }; 44 | 45 | this.onMouseMoveHandle = (event: MouseEvent) => { 46 | this.props.engine.getEventBus().fireEvent(new MouseMoveEvent(this, event.clientX, event.clientY)); 47 | }; 48 | 49 | this.onMouseDownHandle = (event: MouseEvent) => { 50 | this.props.engine.getEventBus().fireEvent(new MouseDownEvent(this, event.clientX, event.clientY)); 51 | }; 52 | 53 | this.onMouseUpHandle = (event: MouseEvent) => { 54 | this.props.engine.getEventBus().fireEvent(new MouseUpEvent(this, event.clientX, event.clientY)); 55 | }; 56 | 57 | this.onMouseWheelHandle = event => { 58 | this.props.engine 59 | .getEventBus() 60 | .fireEvent( 61 | new MouseWheelEvent(this, event.clientX, event.clientY, CanvasWidget.normalizeScrollWheel(event)) 62 | ); 63 | event.stopPropagation(); 64 | event.preventDefault(); 65 | }; 66 | } 67 | 68 | static normalizeScrollWheel(event: WheelEvent) { 69 | let scrollDelta = event.deltaY; 70 | // check if it is pinch gesture 71 | if (event.ctrlKey && scrollDelta % 1 !== 0) { 72 | /* 73 | Chrome and Firefox sends wheel event with deltaY that 74 | have fractional part, also `ctrlKey` prop of the event is true 75 | though ctrl isn't pressed 76 | */ 77 | return (scrollDelta /= 3); 78 | } 79 | return (scrollDelta /= 60); 80 | } 81 | 82 | componentWillMount() { 83 | this.props.engine.setCanvasWidget(this); 84 | } 85 | 86 | componentDidMount() { 87 | document.addEventListener("mousemove", this.onMouseMoveHandle); 88 | document.addEventListener("keydown", this.onKeyDownHandle); 89 | document.addEventListener("keyup", this.onKeyUpHandle); 90 | } 91 | 92 | componentWillUnmount() { 93 | document.removeEventListener("mousemove", this.onMouseMoveHandle); 94 | document.removeEventListener("keyup", this.onKeyUpHandle); 95 | document.removeEventListener("keydown", this.onKeyDownHandle); 96 | this.props.engine.setCanvasWidget(null); 97 | } 98 | 99 | getViewPort(): Rectangle { 100 | let model = this.props.engine.getModel(); 101 | return new Rectangle( 102 | -model.getOffsetX() / model.getZoomLevel(), 103 | -model.getOffsetY() / model.getZoomLevel(), 104 | this.dimension.realDimensions.getWidth() / model.getZoomLevel(), 105 | this.dimension.realDimensions.getHeight() / model.getZoomLevel() 106 | ); 107 | } 108 | 109 | zoomToFit(margin: number = 0) { 110 | let model = this.props.engine.getModel(); 111 | let bounds = Rectangle.boundingBoxFromPolygons( 112 | _.filter( 113 | _.map(model.getElements(), element => { 114 | return element.getDimensions(); 115 | }), 116 | el => { 117 | return !!el; 118 | } 119 | ) 120 | ); 121 | 122 | let zoomFactor = Math.min( 123 | (this.dimension.realDimensions.getWidth() - margin - margin) / bounds.getWidth(), 124 | (this.dimension.realDimensions.getHeight() - margin - margin) / bounds.getHeight() 125 | ); 126 | 127 | model.setZoomLevel(zoomFactor); 128 | model.setOffset( 129 | margin + -1 * bounds.getTopLeft().x * model.getZoomLevel(), 130 | margin + -1 * bounds.getTopLeft().y * model.getZoomLevel() 131 | ); 132 | this.forceUpdate(); 133 | } 134 | 135 | render() { 136 | return ( 137 | 138 |
145 | {_.map(this.props.engine.getModel().layers.getArray(), layer => { 146 | return React.cloneElement( 147 | this.props.engine.getFactoryForElement(layer).generateWidget(this.props.engine, layer), 148 | { 149 | key: layer.getID() 150 | } 151 | ); 152 | })} 153 |
154 |
155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "name": "storm-react-diagrams", 4 | "compilerOptions": { 5 | "suppressExcessPropertyErrors": true, 6 | "declaration": true, 7 | "outDir": "@types", 8 | "target": "es5", 9 | "strictNullChecks": false, 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "jsx": "react", 13 | "baseUrl": ".", 14 | "experimentalDecorators": true, 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts", 23 | "./dist" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "member-access": false, 9 | "comment-format": false, 10 | "max-line-length": false, 11 | "object-literal-sort-keys": false, 12 | "quotemark": [true, "double", "jsx-double"], 13 | "arrow-parens": false, 14 | "indent": [true, "tabs", 2], 15 | "semicolon": false, 16 | "object-literal-key-quotes": [true, "as-needed"], 17 | "no-var-keyword": false, 18 | "jsdoc-format": false, 19 | "prefer-const": false, 20 | "interface-name": false, 21 | "array-type": false, 22 | "trailing-comma": false, 23 | "one-line": false, 24 | "object-literal-shorthand": false, 25 | "no-string-literal": false, 26 | "ordered-imports": false, 27 | "prefer-for-of": false, 28 | "no-empty-interface": false 29 | }, 30 | "rulesDirectory": [] 31 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | const production = process.env.NODE_ENV === "production"; 6 | let plugins = []; 7 | 8 | if (production) { 9 | console.log("creating production build"); 10 | plugins.push( 11 | new webpack.DefinePlugin({ 12 | "process.env.NODE_ENV": '"production"' 13 | }) 14 | ); 15 | } 16 | 17 | module.exports = { 18 | entry: "./src/main.ts", 19 | output: { 20 | filename: "main.js", 21 | path: __dirname + "/dist", 22 | libraryTarget: "umd", 23 | library: "storm-react-canvas" 24 | }, 25 | externals: [nodeExternals()], 26 | plugins: plugins, 27 | module: { 28 | rules: [ 29 | { 30 | enforce: "pre", 31 | test: /\.js$/, 32 | loader: "source-map-loader" 33 | }, 34 | { 35 | test: /\.tsx?$/, 36 | loader: "ts-loader" 37 | } 38 | ] 39 | }, 40 | resolve: { 41 | extensions: [".tsx", ".ts", ".js"] 42 | }, 43 | devtool: production ? "source-map" : "cheap-module-source-map", 44 | mode: production ? "production" : "development", 45 | optimization: { 46 | minimizer: [ 47 | new UglifyJsPlugin({ 48 | uglifyOptions: { 49 | compress: false, 50 | ecma: 5, 51 | mangle: false 52 | }, 53 | sourceMap: true 54 | }) 55 | ] 56 | } 57 | }; 58 | --------------------------------------------------------------------------------