├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── DataModel.ts ├── DefaultConfig.ts ├── EventObject.ts ├── GraphEditor.ts ├── GraphManager.ts ├── GraphViewer.ts ├── SelectionManager.ts ├── TemcEventSource.ts ├── Utils.ts ├── command │ ├── AttributeChange.ts │ ├── Change.ts │ ├── Command.ts │ ├── GeometryChange.ts │ ├── GroupChange.ts │ ├── NodeChange.ts │ ├── OrderChange.ts │ └── UndoRedoManager.ts ├── constants │ ├── EventType.ts │ ├── StyleMap.ts │ └── TemcConstants.ts ├── index.all.node.ts ├── index.all.ts ├── lib │ ├── GifuctLib.ts │ └── gifuct.ts ├── model │ ├── ArcNode.ts │ ├── BaseConnectedLineNode.ts │ ├── CircleNode.ts │ ├── ContainerNode.ts │ ├── EditableShapeNode.ts │ ├── EllipseNode.ts │ ├── GroupNode.ts │ ├── ImageNode.ts │ ├── LabelNode.ts │ ├── LineArrowNode.ts │ ├── LineNode.ts │ ├── Node.ts │ ├── PathNode.ts │ ├── PenNode.ts │ ├── PolylineArrowNode.ts │ ├── PolylineNode.ts │ ├── RectNode.ts │ ├── RegularPolygonNode.ts │ ├── RingNode.ts │ ├── ShapeNode.ts │ ├── StarNode.ts │ ├── StraightConnectedLineNode.ts │ ├── SymbolNode.ts │ ├── TextNode.ts │ └── WedgeNode.ts ├── shape │ ├── AbstractShape.ts │ ├── ArrowShape.ts │ ├── BaseConnectedLineShape.ts │ ├── BaseLineShape.ts │ ├── BasePolylineShape.ts │ ├── LineShape.ts │ ├── PenShape.ts │ ├── PolylineArrowShape.ts │ ├── PolylineShape.ts │ └── StraightConnectedLineShape.ts ├── snapGrid │ └── SnapGrid.ts └── types │ ├── common.ts │ ├── config.ts │ ├── index.ts │ └── snap-grid.ts ├── test ├── css │ └── layui.css ├── js │ ├── jquery.js │ ├── jsoneditor.min.js │ └── layui.js ├── performance-test.html ├── sandbox.html ├── shape │ └── basic.ts ├── unit-tests.html └── unit │ ├── Animation-test.ts │ ├── Arc-test.ts │ ├── Circle-test.ts │ ├── DataModel-test.ts │ ├── Ellipse-test.ts │ ├── GraphEditor-test.ts │ ├── Group-test.ts │ ├── Image-test.ts │ ├── Label-test.ts │ ├── Line-test.ts │ ├── Path-test.ts │ ├── Pen-test.ts │ ├── Polyline-test.ts │ ├── Rect-test.ts │ ├── RegularPolygon-test.ts │ ├── Ring-test.ts │ ├── Star-test.ts │ ├── Symbol-test.ts │ ├── Text-test.ts │ ├── Utils-test.ts │ ├── Wedge-test.ts │ └── test-utils.ts ├── tsconfig-cmj.json ├── tsconfig.json └── typedoc.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | dist 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | .parcel-cache 64 | docs 65 | output 66 | esm 67 | cmj 68 | umd 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.6 2024-06-25 2 | ### Fixed 3 | 1 解决绘制模式下,有时画笔消失的问题 4 | 5 | ## 1.1.2 2024-06-14 6 | ### Fixed 7 | 1 解决锁定不生效的问题 8 | 2 解决对齐不生效的问题 9 | 10 | ## 1.1.1 2024-06-12 11 | ### Added 12 | 1 添加对Context对象的支持 13 | 2 添加镜像翻转功能 14 | 15 | ## 1.1.0 2024-06-11 16 | ### Fixed 17 | 1 解决背景色在拖动过程中,不同步的问题。 18 | 2 解决图元中有动画时,移动位置,动画位置错位的问题。 19 | ### Added 20 | 1 添加画布锁定功能 21 | 2 添加画布适配方式功能 22 | 3 添加画布对齐功能 23 | 4 添加画布适应内容功能 24 | 25 | ## 1.0.99 2023-10-23 26 | ### Fixed 27 | 1 解决条件脚本中,不支持viewer导致的错误。 28 | 29 | 30 | ## 1.0.98 2023-10-18 31 | ### Fixed 32 | 1 预览时,cpu使用率能降低一些 33 | 2 解决如果图形中含有动画则使用fitView方法动画会变形的问题 34 | 3 添加快捷键V,可切换到选择模式 35 | 36 | ## 1.0.97 2023-10-16 37 | ### Added 38 | 1 添加fitView方法,可以让图在指定宽高的容器中占满 39 | 40 | ## 1.0.96 2023-10-13 41 | ### Added 42 | 1 折线路径添加shift,绘制直路径的功能 43 | ### Fixed 44 | 1 解决在绘制过程中,鼠标进行其他操作,会出错的问题 45 | 46 | ## 1.0.95 2023-10-13 47 | ### Fixed 48 | 1 解决clear方法中错误销毁stage的问题 49 | 50 | ## 1.0.93 2023-10-12 51 | ### Fixed 52 | 1 解决select+A全选有时候不生效的问题 53 | 2 解决快捷键撤销,一次撤销两步的问题 54 | 55 | ### Added 56 | 1 添加ctrl+shift+方向键的微调功能 57 | 2 添加判断,当组内有图元时,不允许再次成组 58 | 59 | ## 1.0.92 2023-10-11 60 | ### Added 61 | 1 解决addNode方法中动画必填的问题 62 | 63 | ## 1.0.91 2023-10-11 64 | ### Added 65 | 1 解决addNode方法中属性类型不匹配的问题 66 | 67 | ## 1.0.90 2023-09-26 68 | ### Added 69 | 1 解决删除gif图片删除后,未销毁,导致内存未能下降的问题 70 | 71 | 72 | 73 | ## 1.0.89 2023-09-26 74 | ### Added 75 | 1 解决多个gif相互影响的问题 76 | 77 | ## 1.0.88 2023-09-25 78 | ### Added 79 | 1 解决ImageNode的image未定义的情况,会出错的问题 80 | 2 解决事件条件中含有运算target中为0情况时,事件不响应的问题 81 | 3 insertImage添加对gif图片的支持 82 | 83 | ## 1.0.87 2023-09-21 84 | ### Added 85 | 1 添加对服务器图片的支持 86 | 2 解决是否可以构建图元的状态返回有误的问题 87 | 88 | ## 1.0.86 2023-09-20 89 | ### Fixed 90 | 1 增大hitStrokeWidth,方便直线的选中 91 | 2 解决旋转角度为0时,属性并未存上的问题 92 | 3 减少默认微调的步数,更好的精细控制 93 | 4 解决多选的时候,属性并未取交集的问题 94 | 5 解决直线坐标中含有0时,未能正确渲染的问题 95 | 96 | ## 1.0.85 2023-09-19 97 | ### Fixed 98 | 1 解决onNodeAttributes监听的返回值canvasAction错误的问题 99 | 100 | ## 1.0.84 2023-09-18 101 | ### Fixed 102 | 1 解决updateVariable传入null的属性,可能报错的问题 103 | 104 | 105 | ## 1.0.83 2023-09-15 106 | ### Fixed 107 | 1 解决快捷键阻止冒泡后,撤销不能用的问题 108 | 2 解决事件中条件为运算类型时,不生效的问题 109 | 110 | ## 1.0.82 2023-09-15 111 | ### Fixed 112 | 1 宽度和高度归类为矩形,图片的独有属性 113 | 114 | ## 1.0.81 2023-09-15 115 | ### Fixed 116 | 1 修改事件中执行脚本的传参 117 | 118 | ## 1.0.80 2023-09-14 119 | ### Fixed 120 | 1 解决绑定事件动作执行,如果有触发条件的情况下,未能正确动作的问题 121 | 2 GraphViewer去掉init方法 122 | 123 | ## 1.0.78 2023-09-14 124 | ### Fixed 125 | 1 解决页面中存在两个画布情况下,背景色错乱的问题 126 | 127 | ## 1.0.77 2023-09-14 128 | ### Fixed 129 | 1 解决1.0.75的修改导致的GraphViewer构建的时候传入graph出错的问题 130 | 2 单个图形也可以组成图元 131 | 132 | 133 | ## 1.0.76 2023-09-14 134 | ### Fixed 135 | 1 解决预览属性变化事件不生效的问题 136 | 137 | ## 1.0.75 2023-09-13 138 | ### Fixed 139 | 1 解决背景色不生效的问题 140 | 141 | ## 1.0.74 2023-09-11 142 | ### Fixed 143 | 1 解决图片在有旋转动画的情况下,如果移动位置,动画消失的问题 144 | 2 事件中的attributes属性值由数组改为json对象 145 | 146 | ## 1.0.73 2023-09-08 147 | ### Added 148 | 1 添加对事件中的trigger的增删改的方法 149 | 150 | ## 1.0.72 2023-09-08 151 | ### Fixed 152 | 1 修改事件的结构,同时添加一个事件对多个条件的支持 153 | 154 | ## 1.0.71 2023-09-07 155 | ### Added 156 | 1 解决setAttributeValues在修改rotation及其他的情况下,其他属性未能生效的问题 157 | 158 | ## 1.0.70 2023-09-07 159 | ### Added 160 | 1 解决addEvent参数类型不正确的问题 161 | 162 | ## 1.0.69 2023-09-07 163 | ### Added 164 | 1 添加getNodesAttributes方法,用于选中多个的节点,获取交集属性 165 | 166 | ## 1.0.67 2023-09-05 167 | ### Fixed 168 | 1 deleteVariable,getAttributes方法可以不传id 169 | 170 | 171 | ## 1.0.66 2023-08-31 172 | ### Added 173 | 1 添加getGridConfig方法和getStyleConfig方法 174 | 175 | 176 | ## 1.0.65 2023-08-29 177 | ### Fixed 178 | 1 constructSymbol方法返回对象 179 | 2 解决组成图元和解构图元可用状态不对的问题 180 | 181 | ## 1.0.64 2023-08-28 182 | ### Fixed 183 | 1 解决删除一个有动画的形状,撤销操作错误的问题 184 | 2 添加onFnStateChanged方法 185 | 186 | 187 | ## 1.0.63 2023-08-28 188 | ### Fixed 189 | 1 组成图元的方法修改为constructSymbol,拆解图元的方法改为deConstructSymbol 190 | 2 添加获取背景色的方法 191 | 3 读取状态中修改order的状态 192 | 193 | 194 | ## 1.0.62 2023-08-23 195 | ### Fixed 196 | 1 解决背景色设置位置不正确的问题 197 | 198 | ## 1.0.58 2023-08-23 199 | ### Fixed 200 | 1 添加OrderDirection枚举 201 | 202 | ## 1.0.56 2023-08-23 203 | ### Fixed 204 | 1 添加clear方法 205 | 206 | ## 1.0.55 2023-08-16 207 | ### Fixed 208 | 1 解决编辑态转成普通状态时,编辑态的锚点仍然存在的问题 209 | 2 解决图元或组中存在动画时,动画不响应的问题 210 | 211 | ## 1.0.54 2023-08-16 212 | ### Fixed 213 | 1 解决获取动画的默认周期出错问题 214 | 215 | ## 1.0.53 2023-08-09 216 | ### Fixed 217 | 1 GraphViewer添加init接口,用于解析绑定的事件 218 | 219 | ## 1.0.52 2023-08-08 220 | ### Fixed 221 | 1 添加getVariable接口 222 | 223 | ## 1.0.51 2023-08-08 224 | ### Fixed 225 | 1 解决更新变量没有外部通知的问题 226 | 227 | ## 1.0.50 2023-08-08 228 | ### Fixed 229 | 1 解决预览时不传入数据,出错的问题 230 | 231 | ## 1.0.49 2023-08-08 232 | ### Fixed 233 | 1 解决打包umd下解析不了的问题 234 | 235 | ## 1.0.48 2023-08-07 236 | ### Fixed 237 | 1 解决快捷键影响外部定义的快捷键的问题 238 | 239 | ## 1.0.47 2023-08-07 240 | ### Fixed 241 | 1 绘制折线以双击结束 242 | 243 | ## 1.0.47 2023-08-07 244 | ### Fixed 245 | 1 添加命名空间Giraffe 246 | 247 | 248 | ## 1.0.45 2023-08-07 249 | ### Fixed 250 | 1 解决对预览不识别组内元素的问题 251 | 252 | ## 1.0.44 2023-08-07 253 | ### Fixed 254 | 1 解决流动动画不生效的问题 255 | 2 添加对Utils工具类的导出 256 | 257 | 258 | ## 1.0.43 2023-08-01 259 | ### Fixed 260 | 1 解决对齐辅助线在画布比例发生变化时,不能在正确位置出现的问题,优化设计,提高对齐辅助线的效率 261 | 2 解决旋转的组元素,移动过程中坐标混乱的问题 262 | 3 起点和终点完全一样的直线,不予绘制,防止误操作而产生的无意义元素 263 | 4 解决绘制模式下,可能会触发移动事件的问题 264 | 5 添加右键菜单解决方案 265 | 6 添加剪切功能 266 | 7 解决组元素在复制过程中子元素未重新生成id的问题 267 | 8 解决图元拖拽到画布上的对象都是同一个对象的问题 268 | 269 | 270 | ## 1.0.42 2023-06-13 271 | ### Fixed 272 | 1 将保存变量saveVariable拆分为addVariable和updateVariable方法 273 | 274 | ## 1.0.41 2023-06-12 275 | ### Fixed 276 | 1 事件中改变属性和执行脚本用不同的属性来表示,执行脚本用fnJs来存储 277 | 278 | ## 1.0.40 2023-06-12 279 | ### Added 280 | 1 解决某些情况下,updateEvent导致的value为空的问题 281 | 282 | ## 1.0.39 2023-06-07 283 | ### Added 284 | 1 GraphViewer中的refreshGraph添加传参设置 285 | 286 | ## 1.0.38 2023-06-06 287 | ### Fixed 288 | 1 解决带有旋转的图形的组,再解组后,停止动画,图形不能恢复原来状态的问题 289 | 290 | ## 1.0.38 2023-06-06 291 | ### Fixed 292 | 1 解决五角星在旋转状态下,移动位置,形状消失的问题 293 | 2 解决旋转的图形成组后,整个组移动过程中,旋转中心偏移的问题 294 | 295 | ## 1.0.35 2023-06-05 296 | ### Fixed 297 | 1. 解决元素在有动画的情况下,成组后,旋转中心偏移的问题 298 | 299 | ## 1.0.34 2023-06-02 300 | ### Fixed 301 | 1. 解决设置属性为rotation时,角度叠加的问题 302 | 303 | ## 1.0.33 2023-06-01 304 | ### Fixed 305 | 1. 解决正在旋转的元素,drag起始位置有问题的问题 306 | 307 | ## 1.0.32 2023-05-30 308 | ### Fixed 309 | 1. 解决非100%显示比例下,旋转动画有错的问题 310 | 2. 快捷键配置增加对shift的支持 311 | 312 | ## 1.0.28 2023-05-29 313 | ### Fixed 314 | 1. 解决图形中有动画时,预览错误的问题 315 | 2. 解决预览模式下,拖动画布会导致旋转中心偏离的问题 316 | 3. 修改getAttributesValues方法 317 | 4. 解决resize操作,撤销有时出错的问题 318 | 319 | ## 1.0.25 2023-05-26 320 | ### Added 321 | 1. 解决预览模式下在某些情况下元素可拖拽的问题 322 | 323 | ## 1.0.23 2023-05-25 324 | ### Added 325 | 1. GraphViewer添加setGraph接口 326 | 2. 解决水平等距的形状,在坐标相同的情况下,出现错误的问题 327 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

graph-editor-core

3 |

HTML5 Canvas Library Based on Konva

4 |
5 | 6 | ## Install 7 | 8 | #### NPM 9 | 10 | ```js 11 | npm install graph-editor-core --save 12 | ``` 13 | 14 | #### Yarn 15 | ```js 16 | yarn add graph-editor-core 17 | ``` 18 | 19 | 20 | 21 | 22 | #### Umd 23 | graph-editor-core also supports UMD loading 24 | 25 | ```js 26 | 27 | ``` 28 | 29 | 30 | ## Getting Started 31 | 32 | ```js 33 | const editor = new GraphEditor({ 34 | container: document.getElementById(''), 35 | }) 36 | ``` 37 | 38 | ## Documentation 39 | [Full Documentation](https://unpkg.com/graph-editor-core/docs/index.html) 40 | [CHANGELOG](https://unpkg.com/graph-editor-core/CHANGELOG.md) 41 | 42 | ## License 43 | According to the terms of the [MIT license](LICENSE), graph-editor-core is freely distributable. 44 | 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovekobe24/graph-editor-core/b4170d16bf5c530da800548f395b50286cef98d8/index.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-editor-core", 3 | "version": "1.1.6", 4 | "description": "", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/lovekobe24/graph-editor-core" 8 | }, 9 | "files": [ 10 | "README.md", 11 | "CHANGELOG.md", 12 | "esm", 13 | "cmj", 14 | "umd", 15 | "docs" 16 | ], 17 | "typings": "./esm/index.all.d.ts", 18 | "main": "./esm/index.all.js", 19 | "type": "module", 20 | "scripts": { 21 | "compile": "npm run clean && npm run tsc && npm run rollup", 22 | "start": "npm run test:watch", 23 | "test:watch": "parcel serve ./test/unit-tests.html ./test/sandbox.html ./test/performance-test.html --dist-dir=output --port 51161 --no-cache", 24 | "clean": "rm -rf ./umd && rm -rf ./types && rm -rf ./cmj && rm -rf ./test-build && rm -rf ./esm", 25 | "tsc": "tsc --removeComments && tsc --build ./tsconfig-cmj.json", 26 | "rollup": "rollup -c --bundleConfigAsCjs", 27 | "doc": "typedoc --options ./typedoc.json" 28 | }, 29 | "author": "", 30 | "license": "ISC", 31 | "devDependencies": { 32 | "@rollup/plugin-node-resolve": "^15.0.2", 33 | "@rollup/plugin-terser": "^0.4.1", 34 | "@types/jest": "^29.5.1", 35 | "assert": "^2.0.0", 36 | "parcel": "^2.8.3", 37 | "process": "^0.11.10", 38 | "rollup": "^3.21.6", 39 | "rollup-plugin-commonjs": "^10.1.0", 40 | "rollup-plugin-dts": "^5.3.0", 41 | "rollup-plugin-ignore": "^1.0.10", 42 | "rollup-plugin-replace": "^2.2.0", 43 | "rollup-plugin-typescript2": "^0.34.1", 44 | "ts-loader": "^9.4.2", 45 | "typedoc": "^0.24.7", 46 | "typescript": "^4.9.5", 47 | "webpack": "^5.76.0", 48 | "webpack-cli": "^5.0.1" 49 | }, 50 | "dependencies": { 51 | "@rollup/plugin-inject": "^5.0.3", 52 | "gifuct-js": "^2.1.2", 53 | "konva": "^9.3.11" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve' 4 | import ignore from 'rollup-plugin-ignore' 5 | import inject from '@rollup/plugin-inject' 6 | import path from 'path' 7 | import replace from 'rollup-plugin-replace'; 8 | import pkg from './package.json'; 9 | 10 | export default [ 11 | { 12 | input: `src/GraphEditor.ts`, 13 | output: [ 14 | { 15 | file: 'umd/graph-editor.js', 16 | name: 'GraphEditor', 17 | format: 'iife', 18 | sourcemap: false, 19 | freeze: false, 20 | banner: `/*! 21 | * GraphEditor v${pkg.version} 22 | * (c) ${new Date().getFullYear()} Zhang Na 23 | * License: MIT 24 | */` 25 | } 26 | ], 27 | plugins: [ 28 | ignore(['canvas']), 29 | nodeResolve(), 30 | commonjs(), 31 | typescript(), 32 | replace({ 33 | delimiters: ['', ''], 34 | '__VERSION__': pkg.version 35 | }) 36 | ], 37 | }, 38 | { 39 | input: `src/GraphViewer.ts`, 40 | output: [ 41 | { 42 | file: 'umd/graph-viewer.js', 43 | name: 'GraphViewer', 44 | format: 'iife', 45 | sourcemap: false, 46 | freeze: false, 47 | banner: `/*! 48 | * GraphViewer v${pkg.version} 49 | * (c) ${new Date().getFullYear()} Zhang Na 50 | * License: MIT 51 | */` 52 | } 53 | ], 54 | plugins: [ 55 | ignore(['canvas']), 56 | nodeResolve(), 57 | commonjs(), 58 | typescript(), 59 | replace({ 60 | delimiters: ['', ''], 61 | '__VERSION__': pkg.version 62 | }) 63 | ], 64 | }, 65 | { 66 | input: `src/index.all.ts`, 67 | output: [ 68 | { 69 | file: 'umd/graph-editor-core.js', 70 | name: 'Giraffe', 71 | format: 'iife', 72 | sourcemap: false, 73 | freeze: false, 74 | banner: `/*! 75 | * Giraffe v${pkg.version} 76 | * (c) ${new Date().getFullYear()} Zhang Na 77 | * License: MIT 78 | */` 79 | } 80 | ], 81 | plugins: [ 82 | ignore(['canvas']), 83 | nodeResolve(), 84 | commonjs(), 85 | typescript(), 86 | replace({ 87 | delimiters: ['', ''], 88 | '__VERSION__': pkg.version 89 | }) 90 | ], 91 | }, 92 | { 93 | plugins: [ 94 | ignore(['canvas']), 95 | nodeResolve(), 96 | commonjs({ 97 | exclude: '/node_modules/' 98 | }), 99 | typescript({ 100 | tsconfigOverride: { 101 | compilerOptions: { declaration: true } 102 | }, 103 | outDir: 'esm', 104 | declarationDir: 'esm' 105 | }), 106 | inject({ 107 | global: path.resolve('./build/global') 108 | }), 109 | //terser() 110 | ], 111 | input: 'src/index.all.ts', 112 | preserveModules: false, 113 | output: { 114 | dir: 'esm', 115 | format: 'esm' 116 | } 117 | } 118 | ] -------------------------------------------------------------------------------- /src/DefaultConfig.ts: -------------------------------------------------------------------------------- 1 | import type { EditorConfig } from "./types"; 2 | 3 | export const defaultConfig:EditorConfig = { 4 | view: { 5 | grid: { 6 | show: true, 7 | distance: 50, 8 | color: '#cccccc' 9 | }, 10 | size: { 11 | width: 1000, 12 | height: 800 13 | } 14 | }, 15 | drawing: { 16 | snapToGrid: { 17 | enable: false, 18 | strokeWidth: 2, 19 | stroke: '#067BEF', 20 | dash: [3, 6] 21 | }, 22 | editAnchor: { 23 | anchorFill: 'white', 24 | anchorStroke: 'rgb(0, 161, 255)' 25 | }, 26 | connectable:false 27 | }, 28 | selection: { 29 | transformer: { 30 | anchorSize: 8, 31 | rotateAnchorOffset: 30, 32 | anchorStroke: 'rgb(0, 161, 255)', 33 | anchorStrokeWidth:1, 34 | anchorFill: 'white', 35 | borderStroke: 'rgb(0, 161, 255)', 36 | borderWidth: 5 37 | }, 38 | zone: { 39 | fill: 'rgba(105, 105, 105, 0.7)', 40 | stroke: '#dbdbdb' 41 | }, 42 | keyboard: { 43 | enabled: true, 44 | movingSpaces: 1, 45 | map: { 46 | delete: ['Delete'], 47 | moveLeft: ['Control+ArrowLeft'], 48 | moveRight: ['Control+ArrowRight'], 49 | moveUp: ['Control+ArrowUp'], 50 | moveDown: ['Control+ArrowDown'], 51 | moveLeftSlow: ['Control+Shift+ArrowLeft'], 52 | moveRightSlow: ['Control+Shift+ArrowRight'], 53 | moveUpSlow: ['Control+Shift+ArrowUp'], 54 | moveDownSlow: ['Control+Shift+ArrowDown'], 55 | selectAll: ['Control+a'], 56 | deselect: ['Control+d'], 57 | inverseSelect: ['Control+i'], 58 | copy:['Control+c'], 59 | paste:['Control+v'], 60 | group:['Control+g'], 61 | undo:['Control+z'], 62 | redo:['Control+y'], 63 | toSelectMode:['v'] 64 | } 65 | } 66 | }, 67 | style: { 68 | strokeWidth: 2, 69 | stroke: '#ff0000', 70 | draggable: true, 71 | strokeScaleEnabled: false, 72 | hitStrokeWidth:6 73 | }, 74 | history:{ 75 | keyboard:{ 76 | enabled:true 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/EventObject.ts: -------------------------------------------------------------------------------- 1 | export default class EventObject { 2 | 3 | name: string; 4 | properties: any = {}; 5 | 6 | constructor(name: string, ...params: any) { 7 | this.name = name; 8 | for (let i = 0, len = params.length; i < len; i += 2) { 9 | let key = params[i], val = params[i + 1]; 10 | if (typeof key === 'string' && val !== undefined) { 11 | this.properties[key] = val; 12 | } 13 | } 14 | } 15 | 16 | getName() { 17 | return this.name; 18 | } 19 | 20 | getProperty(key: string) { 21 | return this.properties[key]; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/GraphManager.ts: -------------------------------------------------------------------------------- 1 | import type { Stage } from "konva/lib/Stage"; 2 | import { GRAPH_EDITOR_WARNING, MAX_SCALE, MIN_SCALE,LEFT_ALIGN,TOP_ALIGN,NORMAL_FIT } from "./constants/TemcConstants"; 3 | import type { DataModel } from "./DataModel"; 4 | import { Utils } from "./Utils"; 5 | import Konva from "konva"; 6 | import type { Node } from './model/Node'; 7 | import type { Layer } from "konva/lib/Layer"; 8 | import { ContainerNode, ContainerNodeAttrs } from "./model/ContainerNode"; 9 | import AttributeChange from "./command/AttributeChange"; 10 | import Command from "./command/Command"; 11 | import GeometryChange from "./command/GeometryChange"; 12 | import type { Config } from './types' 13 | 14 | 15 | import { SymbolNode } from './model/SymbolNode'; 16 | import { RectNode } from './model/RectNode'; 17 | import { EllipseNode } from './model/EllipseNode'; 18 | import { CircleNode } from './model/CircleNode'; 19 | import { ImageNode } from './model/ImageNode'; 20 | import { StarNode } from "./model/StarNode"; 21 | import { RegularPolygonNode } from './model/RegularPolygonNode'; 22 | import { ArcNode } from "./model/ArcNode"; 23 | import { PathNode } from "./model/PathNode"; 24 | import { TextNode } from "./model/TextNode"; 25 | import { RingNode } from './model/RingNode'; 26 | import { WedgeNode } from './model/WedgeNode'; 27 | import { LabelNode } from "./model/LabelNode"; 28 | import { GroupNode } from "./model/GroupNode"; 29 | import { LineArrowNode } from "./model/LineArrowNode"; 30 | import { LineNode } from "./model/LineNode"; 31 | import { PenNode } from "./model/PenNode"; 32 | import { PolylineNode } from "./model/PolylineNode"; 33 | import { PolylineArrowNode } from "./model/PolylineArrowNode"; 34 | 35 | /* 36 | * JavaScript Framework v@@version 37 | * Licensed under the MIT 38 | * Date: @@date 39 | */ 40 | export abstract class GraphManager { 41 | container: HTMLDivElement | null; 42 | config: Config = {}; 43 | nodeLayer: Layer = new Konva.Layer(); 44 | //背景绘制层 45 | backgroundColorLayer: Layer = new Konva.Layer({ listening: false, name: '背景层' }); 46 | backgroundRect:any; 47 | dataModel: DataModel | undefined; 48 | stage: Stage | undefined; 49 | width: number | undefined; 50 | height: number | undefined; 51 | /** 52 | * The settings 53 | */ 54 | name: string = ''; 55 | //画布是否锁定 56 | locked:boolean=true; 57 | //水平对齐 58 | hAlign:String=LEFT_ALIGN; 59 | //垂直对齐 60 | vAlign:String=TOP_ALIGN; 61 | //显示方式设置 62 | fit:string=NORMAL_FIT; 63 | 64 | constructor(config: Config) { 65 | if (Utils.isBrowser() && !config.container) { 66 | throw new Error(GRAPH_EDITOR_WARNING + 'It needs to have a container element') 67 | } 68 | } 69 | 70 | /** 71 | * 修改视图的显示比例 72 | * @param scale 显示比例 73 | */ 74 | setScale(scale: number) { 75 | if (scale > MAX_SCALE) { 76 | scale = MAX_SCALE 77 | } 78 | if (scale < MIN_SCALE) { 79 | scale = MIN_SCALE 80 | } 81 | if (this.stage) { 82 | this.stage.scaleX(scale); 83 | this.stage.scaleY(scale); 84 | if (this.width) { 85 | this.stage.setAttr('width', this.width * scale); 86 | } 87 | if (this.height) { 88 | this.stage.setAttr('height', this.height * scale); 89 | } 90 | } 91 | this.dataModel?.getNodes().forEach((node: any) => { 92 | node.updateRefAnimation("scaleChange"); 93 | }); 94 | } 95 | 96 | /** 97 | * 获取当前画布的显示比例 98 | */ 99 | getScale() { 100 | return this.stage?.getAttr('scaleX'); 101 | } 102 | 103 | /** 104 | * 根据节点id获取节点 105 | * @param nodeId 节点id 106 | */ 107 | getNode(nodeId: string) { 108 | return this.dataModel ? this.dataModel.getNodeById(nodeId) : null; 109 | } 110 | 111 | 112 | setSize(width: number, height: number) { 113 | this.width = width; 114 | this.height = height; 115 | let scale = this.stage?.getAttr('scaleX'); 116 | this.stage?.setAttr('width', width * scale); 117 | this.stage?.setAttr('height', height * scale); 118 | //修改背景矩形 119 | this.backgroundRect.setAttr('width', width * scale); 120 | this.backgroundRect.setAttr('height', height * scale); 121 | } 122 | 123 | /** 124 | * 获取画布的宽度和高度 125 | */ 126 | getSize(){ 127 | return {width:this.width,height:this.height} 128 | } 129 | 130 | /** 131 | * 加载指定的图形 132 | * @param graphContent 图形信息的字符串 133 | */ 134 | setGraph(graphContent: any) { 135 | if (this.stage?.getChildren().length == 0) 136 | return 137 | //清空原来的数据 138 | this.clear(); 139 | let graphJson = JSON.parse(graphContent); 140 | this.setSize(graphJson.width, graphJson.height); 141 | let bgColor = graphJson.backgroundColor; 142 | if (bgColor) { 143 | this.setBackgroundColor(bgColor); 144 | } 145 | if (graphJson.name) { 146 | this.name = graphJson.name; 147 | } 148 | if(graphJson.hAlign){ 149 | this.hAlign=graphJson.hAlign; 150 | } 151 | if(graphJson.vAlign){ 152 | this.vAlign=graphJson.vAlign; 153 | } 154 | if(graphJson.fit){ 155 | this.fit=graphJson.fit; 156 | } 157 | if(graphJson.locked!=undefined){ 158 | this.locked=graphJson.locked; 159 | } 160 | 161 | this.dataModel?.fromObject(graphJson.model); 162 | } 163 | fitView(width: number, height: number) { 164 | if (this.width && this.height) { 165 | let scaleX = width / this.width; 166 | let scaleY = height / this.height; 167 | this.stage?.scaleX(scaleX); 168 | this.stage?.scaleY(scaleY); 169 | this.stage?.setAttr('width', width); 170 | this.stage?.setAttr('height', height); 171 | this.dataModel?.getNodes().forEach((node: any) => { 172 | node.updateRefAnimation("scaleChange"); 173 | }); 174 | } 175 | 176 | } 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | /** 186 | * 清空画布内容 187 | */ 188 | clear() { 189 | this.dataModel?.clear(); 190 | } 191 | /** 192 | * 销毁画布 193 | */ 194 | destroy() { 195 | this.dataModel?.destroy(); 196 | delete this.dataModel; 197 | this.stage?.destroy(); 198 | 199 | } 200 | 201 | /** 202 | * 获取画布上所有的节点 203 | */ 204 | getNodes() { 205 | let nodes: any = []; 206 | this.dataModel?.getNodes().forEach((element: any) => { 207 | nodes.push(element.toObject()); 208 | }); 209 | return nodes; 210 | } 211 | getSetNodesAttributesChange(nodes: Array, attrValues: any, toHistory: boolean = true) { 212 | let undoRedoManager = this.dataModel?.getUndoRedoManager(); 213 | let rotationAngle: any = null; 214 | if (attrValues.hasOwnProperty('rotation')) { 215 | rotationAngle = attrValues['rotation']; 216 | delete attrValues['rotation']; 217 | } 218 | // 将预设属性值分成 [全部预设属性值]、[容器节点预设属性值]、[成员节点预设属性值] 三类 219 | let containerAttrKeys = Object.keys(new ContainerNodeAttrs()); 220 | let nextAllAttrValues: any = attrValues, nextContainerAttrValues: any = {}, nextMemberAttrValues: any = {}; 221 | for (let name in nextAllAttrValues) { 222 | let attrValue = nextAllAttrValues[name]; 223 | if (containerAttrKeys.includes(name)) { 224 | nextContainerAttrValues[name] = attrValue; 225 | } else { 226 | nextMemberAttrValues[name] = attrValue; 227 | } 228 | } 229 | let attrValuesMap = new Map(); 230 | for (let node of nodes) { 231 | // 1. 针对顶级节点中的容器节点,缓存[容器节点预设属性值]、[容器节点复归属性值] 232 | if (node instanceof ContainerNode) { 233 | let prevContainerAttrValues: any = {}; 234 | if (toHistory) { 235 | for (let name in nextContainerAttrValues) { 236 | prevContainerAttrValues[name] = node.getAttributeValue(name); 237 | } 238 | } 239 | attrValuesMap.set(node, [prevContainerAttrValues, nextContainerAttrValues]); 240 | // 2. 递归全部节点中的非容器成员节点,缓存[成员节点预设属性值]、[成员节点复归属性值] 241 | (function loop(nodes) { 242 | for (let node of nodes) { 243 | if (node instanceof ContainerNode) { 244 | loop(node.getMembers()); 245 | } else { 246 | let prevMemberAttrValues: any = {}; 247 | if (toHistory) { 248 | for (let name in nextMemberAttrValues) { 249 | prevMemberAttrValues[name] = node.getAttributeValue(name); 250 | } 251 | } 252 | attrValuesMap.set(node, [prevMemberAttrValues, nextMemberAttrValues]); 253 | } 254 | } 255 | })(node.getMembers()); 256 | } 257 | // 3. 针对顶级节点中的非容器节点,缓存[全部预设属性值]、[全部复归属性值] 258 | else { 259 | let prevAllAttrValues: any = {}; 260 | if (toHistory) { 261 | for (let name in nextAllAttrValues) { 262 | prevAllAttrValues[name] = node.getAttributeValue(name); 263 | } 264 | } 265 | 266 | attrValuesMap.set(node, [prevAllAttrValues, nextAllAttrValues]); 267 | } 268 | } 269 | if (this.dataModel) { 270 | if (rotationAngle != null) { 271 | let attrChange = new AttributeChange(attrValuesMap, this.dataModel); 272 | if (attrValues.hasOwnProperty('x') || attrValues.hasOwnProperty('y')) { 273 | //最为复杂的情况x,y,rotation同时修改 274 | undoRedoManager.execute(new Command([attrChange]), false); 275 | } 276 | //如果是手动修改rotation,则需要改变x,y 277 | nodes.forEach((node) => { 278 | let attrs = node.getRef().getClientRect(); 279 | attrs.rotation = 0; 280 | let diff = rotationAngle - node.getRef().getAttr('rotation'); 281 | let rad = Utils.getRad(diff); 282 | const shape = Utils.rotateAroundCenter(attrs, rad); 283 | Utils.fitNodesInto(node.getRef(), attrs, shape); 284 | }) 285 | 286 | let rotateChange = new GeometryChange(nodes, 'rotate', this.dataModel, false); 287 | return [attrChange, rotateChange] 288 | 289 | 290 | } else { 291 | //没有旋转,则直接修改属性 292 | let attrChange = new AttributeChange(attrValuesMap, this.dataModel); 293 | return [attrChange]; 294 | } 295 | 296 | } 297 | 298 | } 299 | setNodesAttributes(nodes: Array, attrValues: any, toHistory: boolean = true) { 300 | let undoRedoManager = this.dataModel?.getUndoRedoManager(); 301 | let changeArray = this.getSetNodesAttributesChange(nodes, attrValues, toHistory); 302 | let cmd = new Command(changeArray); 303 | undoRedoManager.execute(cmd, toHistory); 304 | } 305 | /** 306 | * 设置背景色 307 | * @param color 背景色 308 | * @example 309 | * editor.setBackgroundColor('red'); 310 | */ 311 | setBackgroundColor(color: string) { 312 | if (this.backgroundRect) { 313 | this.backgroundRect.setAttr('fill',color); 314 | } else { 315 | console.warn(GRAPH_EDITOR_WARNING + "未能设置背景色"); 316 | } 317 | } 318 | /** 319 | * 320 | * @returns 获取背景色 321 | */ 322 | getBackgroundColor() { 323 | return this.backgroundRect.getAttr('fill'); 324 | } 325 | } -------------------------------------------------------------------------------- /src/SelectionManager.ts: -------------------------------------------------------------------------------- 1 | import type { DataModel } from "./DataModel"; 2 | import EVENT_TYPE from './constants/EventType'; 3 | import EventObject from './EventObject'; 4 | import { Util } from "konva/lib/Util"; 5 | import {Utils} from "./Utils"; 6 | 7 | export class SelectionManager { 8 | 9 | selectedNodes: any; 10 | dataModel: any; 11 | 12 | constructor(dataModel: DataModel) { 13 | this.selectedNodes = []; 14 | this.dataModel = dataModel; 15 | } 16 | 17 | /** 18 | * 根据节点的id,设置选中的数据模型中选中的节点 19 | * @param nodeIds 20 | * @param fireEvent 21 | */ 22 | setSelection(nodeIds: any, notifyStage: boolean) { 23 | this.selectedNodes = this.dataModel.getNodes().filter((item: any) => { 24 | return nodeIds.includes(item.id) 25 | }) 26 | 27 | this.dataModel.fireEvent(new EventObject(EVENT_TYPE.SELECT_CHANGE, 'nodes',Utils.getObjectNodes(this.selectedNodes)), null); 28 | this.dataModel.fireEvent(new EventObject(EVENT_TYPE.FN_STATE_CHANGE), null); 29 | 30 | if (notifyStage) { 31 | //改变stage中选中的节点元素 32 | this.dataModel.fireEvent(new EventObject(EVENT_TYPE.SELECTION_KONVA_NODE_CHANGE, 'nodes', this.selectedNodes), null); 33 | } 34 | } 35 | 36 | clearSelection() { 37 | this.selectedNodes = []; 38 | } 39 | 40 | getSelection() { 41 | return this.selectedNodes; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/TemcEventSource.ts: -------------------------------------------------------------------------------- 1 | import EventObject from "./EventObject"; 2 | 3 | export default class TemcEventSource { 4 | eventListeners: any; 5 | eventSource: any; 6 | 7 | constructor() { 8 | 9 | } 10 | 11 | addListener(name: string, func: any) { 12 | if (this.eventListeners == null) { 13 | this.eventListeners = []; 14 | } 15 | this.eventListeners.push(name); 16 | this.eventListeners.push(func); 17 | } 18 | 19 | getEventSource() { 20 | return this.eventSource; 21 | } 22 | 23 | fireEvent(evt: any, sender: any) { 24 | if (this.eventListeners != null) { 25 | if (evt == null) { 26 | evt = new EventObject('unknown'); 27 | } 28 | if (sender == null) { 29 | sender = this.getEventSource(); 30 | } 31 | if (sender == null) { 32 | sender = this; 33 | } 34 | let args = [sender, evt]; 35 | for (let i = 0; i < this.eventListeners.length; i += 2) { 36 | let listen = this.eventListeners[i]; 37 | 38 | if (listen == null || listen == evt.getName()) { 39 | this.eventListeners[i + 1].apply(this, args); 40 | } 41 | } 42 | } 43 | }; 44 | 45 | } -------------------------------------------------------------------------------- /src/command/AttributeChange.ts: -------------------------------------------------------------------------------- 1 | import Change from './Change' 2 | import type { Node } from "../model/Node"; 3 | import type { DataModel } from '../DataModel'; 4 | 5 | class AttributeChange extends Change { 6 | 7 | attrValuesMap: Map>; 8 | dataModel: DataModel; 9 | 10 | constructor(attrValuesMap: Map> = new Map(), dataModel: DataModel) { 11 | super(); 12 | this.dataModel = dataModel; 13 | this.attrValuesMap = attrValuesMap; 14 | } 15 | 16 | private apply(flag: boolean) { 17 | let map = new Map(); 18 | for (let [node, attrValuesPairs] of this.attrValuesMap) { 19 | map.set(node, attrValuesPairs[flag ? 1 : 0]); 20 | } 21 | this.dataModel.setAttributeValues(map); 22 | } 23 | 24 | execute() { 25 | this.apply(true); 26 | } 27 | 28 | undo() { 29 | this.apply(false); 30 | } 31 | 32 | } 33 | 34 | export default AttributeChange; -------------------------------------------------------------------------------- /src/command/Change.ts: -------------------------------------------------------------------------------- 1 | abstract class Change { 2 | abstract execute(): void; 3 | abstract undo(): void; 4 | } 5 | 6 | export default Change; -------------------------------------------------------------------------------- /src/command/Command.ts: -------------------------------------------------------------------------------- 1 | import type Change from './Change' 2 | 3 | class Command { 4 | 5 | changes: Array; 6 | 7 | constructor(changes: Array = []) { 8 | this.changes = changes; 9 | } 10 | 11 | undo() { 12 | let len = this.changes.length, i = len - 1; 13 | while (i > -1) this.changes[i--].undo(); 14 | } 15 | 16 | execute() { 17 | let len = this.changes.length, i = 0; 18 | while (i < len) this.changes[i++].execute(); 19 | } 20 | 21 | } 22 | 23 | export default Command; -------------------------------------------------------------------------------- /src/command/GeometryChange.ts: -------------------------------------------------------------------------------- 1 | import { ACTION_TO_STYLE_MAP } from "../constants/StyleMap"; 2 | import type { DataModel } from "../DataModel"; 3 | import { BaseConnectedLineNode } from "../model/BaseConnectedLineNode"; 4 | 5 | import type { Node } from '../model/Node'; 6 | import Change from "./Change"; 7 | 8 | class GeometryChange extends Change { 9 | 10 | attrValuesMap: Map>; 11 | dataModel: DataModel; 12 | canvasAction:boolean; 13 | 14 | constructor(nodes: Array, action: string, dataModel: any,canvasAction:boolean=true) { 15 | super(); 16 | let map = new Map(), actionAttrKeys = ACTION_TO_STYLE_MAP[action]; 17 | for (let node of nodes) { 18 | if(node instanceof BaseConnectedLineNode){ 19 | let konvaNode = node.getRef(); 20 | let prevAllAttrs = node.getAttributeValues(), nextAllAttrs = konvaNode.attrs; 21 | let prevAttrs: any = {}, nextAttrs: any = {}; 22 | let relatedAttKeys=['points']; 23 | relatedAttKeys.forEach((key)=>{ 24 | if(prevAllAttrs.hasOwnProperty(key)) prevAttrs[key] = prevAllAttrs[key]; 25 | let nextAttrValue=nextAllAttrs.hasOwnProperty(key)?nextAllAttrs[key]: konvaNode.getAttr(key); 26 | if(nextAttrValue!=undefined) nextAttrs[key] = nextAttrValue; 27 | }) 28 | map.set(node, [prevAttrs, nextAttrs]); 29 | }else{ 30 | let konvaNode = node.getRef(); 31 | let prevAllAttrs = node.getAttributeValues(), nextAllAttrs = konvaNode.attrs; 32 | let prevAttrs: any = {}, nextAttrs: any = {}; 33 | for (let key of actionAttrKeys) { 34 | if(prevAllAttrs.hasOwnProperty(key)) prevAttrs[key] = prevAllAttrs[key]; 35 | let nextAttrValue=nextAllAttrs.hasOwnProperty(key)?nextAllAttrs[key]: konvaNode.getAttr(key); 36 | //不能简单用if(nextAttrValue)因为nextAttrValue有可能为0 37 | if(nextAttrValue!=undefined) nextAttrs[key] = nextAttrValue; 38 | } 39 | map.set(node, [prevAttrs, nextAttrs]); 40 | } 41 | 42 | } 43 | this.attrValuesMap = map; 44 | this.dataModel = dataModel; 45 | this.canvasAction=canvasAction; 46 | } 47 | 48 | private apply(flag: boolean) { 49 | let map = new Map(); 50 | for (let [node, attrValuesPairs] of this.attrValuesMap) { 51 | map.set(node, attrValuesPairs[flag ? 1 : 0]); 52 | } 53 | this.dataModel.setAttributeValues(map,this.canvasAction); 54 | } 55 | 56 | execute() { 57 | this.apply(true); 58 | } 59 | 60 | undo() { 61 | this.apply(false); 62 | } 63 | 64 | } 65 | 66 | export default GeometryChange; -------------------------------------------------------------------------------- /src/command/GroupChange.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from '../model/Node'; 2 | import { GroupNode } from '../model/GroupNode'; 3 | import Change from './Change'; 4 | import Konva from 'konva'; 5 | import { ContainerNode } from '../model/ContainerNode'; 6 | 7 | class GroupChange extends Change { 8 | 9 | isGroup: boolean = true; 10 | executed: boolean = false; 11 | groups: Array = []; 12 | groupZIndexs: Array = []; 13 | members: Array = []; 14 | memberZIndexs: Array = []; 15 | dataModel: any; 16 | 17 | constructor(nodes: Array, isGroup: boolean, dataModel: any) { 18 | super(); 19 | this.dataModel = dataModel; 20 | this.isGroup = isGroup; 21 | if (isGroup) { 22 | const group = new GroupNode(); 23 | group.setAttributeValue('draggable', true); 24 | group.setMembers(nodes.map((item: any) => { 25 | let node = item.clone(true); 26 | node.setAttributeValue('draggable', false); 27 | return node; 28 | })); 29 | 30 | group.setDataModel(this.dataModel); 31 | this.groups = [group]; 32 | this.groupZIndexs = [-1]; 33 | this.members = nodes; 34 | this.memberZIndexs = nodes.map((item: any) => item.getZIndex()); 35 | } else { 36 | nodes = nodes.filter((item: Node) => item instanceof ContainerNode); 37 | this.groups = nodes; 38 | this.groupZIndexs = nodes.map((item: any) => item.getZIndex()); 39 | for (let node of nodes) { 40 | const _nodes = (node as GroupNode).getMembers(); 41 | for (let _node of _nodes) { 42 | _node.destroyAnimation(); 43 | const _konvaNode = _node.getRef(); 44 | const parentTransform = _konvaNode.getParent().getTransform(); 45 | const newLocalTransform = new Konva.Transform(); 46 | newLocalTransform.multiply(parentTransform.copy()) 47 | .multiply(_konvaNode.getTransform()); 48 | const attrs =newLocalTransform.decompose(); 49 | const member = _node.clone(true); 50 | member.setAttributeValue('scaleX', attrs['scaleX']); 51 | member.setAttributeValue('scaleY', attrs['scaleY']); 52 | member.setAttributeValue('rotation', attrs['rotation']); 53 | member.setAttributeValue('skewX', attrs['skewX']); 54 | member.setAttributeValue('skewY', attrs['skewY']); 55 | member.setAttributeValue('x', attrs['x']); 56 | member.setAttributeValue('y', attrs['y']); 57 | member.setAttributeValue('draggable', true); 58 | this.members.push(member); 59 | this.memberZIndexs.push(-1); 60 | } 61 | } 62 | } 63 | } 64 | 65 | group() { 66 | this.members.forEach(item => { item.remove(); }); 67 | this.groups.forEach((item, index) => { 68 | this.dataModel.addNode(item, this.groupZIndexs[index]); 69 | //旋转的元素成组后,要添加上子元素的动画,因为在ref是新生成的 70 | if(item instanceof GroupNode){ 71 | item.getMembers().forEach((item:Node)=>{ 72 | let autoPlay = item.getAnimationValue('autoPlay'); 73 | if(autoPlay){ 74 | item.updateRefAnimation("group"); 75 | } 76 | }) 77 | } 78 | }); 79 | const groupIds = this.groups.map(item => item.getId()); 80 | this.dataModel.getSelectionManager().setSelection(groupIds, true); 81 | } 82 | 83 | unGroup() { 84 | 85 | this.groups.forEach(item => { item.remove(); }); 86 | this.members.forEach((item, index) => { 87 | this.dataModel.addNode(item, this.memberZIndexs[index]); 88 | }); 89 | const memberIds = this.members.map(item => item.getId()); 90 | this.dataModel.getSelectionManager().setSelection(memberIds, true); 91 | } 92 | 93 | execute() { 94 | if (this.executed === false) { 95 | if (this.isGroup) { 96 | this.group(); 97 | } else { 98 | this.unGroup(); 99 | } 100 | this.executed = true; 101 | } 102 | } 103 | 104 | undo() { 105 | if (this.executed === true) { 106 | if (this.isGroup) { 107 | this.unGroup(); 108 | } else { 109 | this.group(); 110 | } 111 | this.executed = false; 112 | } 113 | } 114 | 115 | } 116 | 117 | export default GroupChange; -------------------------------------------------------------------------------- /src/command/NodeChange.ts: -------------------------------------------------------------------------------- 1 | import type { DataModel } from "../DataModel"; 2 | 3 | class NodeChange { 4 | operateNodes: any; 5 | action: string; 6 | dataModel: DataModel; 7 | nodeToIndex: any; 8 | 9 | constructor(nodes: any, action: string, dataModel: DataModel) { 10 | this.operateNodes = nodes; 11 | this.action = action; 12 | this.dataModel = dataModel; 13 | this.nodeToIndex = new Map(); 14 | this.operateNodes.forEach((item: any) => { 15 | this.nodeToIndex.set(item, item.getZIndex()); 16 | }); 17 | } 18 | 19 | execute() { 20 | switch (this.action) { 21 | case 'add': this.dataModel.addNodes(this.operateNodes,this.nodeToIndex); break; 22 | case 'delete': this.dataModel.removeNodes(this.operateNodes); break; 23 | } 24 | } 25 | 26 | undo() { 27 | switch (this.action) { 28 | case 'add': this.dataModel.removeNodes(this.operateNodes); break; 29 | case 'delete': this.dataModel.addNodes(this.operateNodes,this.nodeToIndex); break; 30 | } 31 | } 32 | } 33 | 34 | export default NodeChange; -------------------------------------------------------------------------------- /src/command/OrderChange.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import EVENT_TYPE from "../constants/EventType"; 4 | import { MOVE_STYLE } from "../constants/StyleMap"; 5 | 6 | import {Utils} from "../Utils"; 7 | import EventObject from "../EventObject"; 8 | class OrderChange { 9 | previousValue: any; 10 | selectNode: any; 11 | dataModel: any; 12 | konvaNode: any; 13 | newIndex: number; 14 | oldIndex: number; 15 | direction: string; 16 | constructor(selectNode: any, direction: string, dataModel: any) { 17 | 18 | this.selectNode = selectNode; 19 | this.dataModel = dataModel; 20 | this.konvaNode = this.selectNode.getRef(); 21 | this.direction = direction; 22 | switch (this.direction) { 23 | case 'up': 24 | this.konvaNode.moveUp(); 25 | break; 26 | case 'down': 27 | this.konvaNode.moveDown(); 28 | break; 29 | case 'top': 30 | this.konvaNode.moveToTop(); 31 | break; 32 | case 'bottom': 33 | this.konvaNode.moveToBottom(); 34 | break; 35 | } 36 | this.newIndex = this.konvaNode.zIndex(); 37 | this.oldIndex = this.dataModel.nodes.indexOf(selectNode); 38 | } 39 | 40 | execute() { 41 | Utils.moveItemInArray(this.dataModel.nodes,this.oldIndex,this.newIndex); 42 | this.selectNode.getRef().zIndex(this.newIndex); 43 | 44 | } 45 | 46 | undo() { 47 | Utils.moveItemInArray(this.dataModel.nodes,this.newIndex,this.oldIndex); 48 | this.selectNode.getRef().zIndex(this.oldIndex); 49 | } 50 | 51 | } 52 | 53 | export default OrderChange; -------------------------------------------------------------------------------- /src/command/UndoRedoManager.ts: -------------------------------------------------------------------------------- 1 | import type { GraphManager } from "../GraphManager"; 2 | import {Utils} from "../Utils"; 3 | 4 | import type Command from "./Command"; 5 | 6 | class UndoRedoManager { 7 | 8 | limit: number = 30; 9 | undos: any = []; 10 | redos: any = []; 11 | settings:any; 12 | 13 | constructor(gm:GraphManager,limit: number) { 14 | 15 | this.settings=gm.config 16 | if (limit > 0) this.limit = limit; 17 | // if (Utils.isBrowser()) { 18 | // window.addEventListener('keydown', this.onKeyDown.bind(this)) 19 | // } 20 | } 21 | 22 | canUndo() { 23 | return this.undos.length > 0; 24 | } 25 | 26 | undo() { 27 | if (this.canUndo()) { 28 | let cmd = this.undos.pop(); 29 | this.redos.push(cmd); 30 | cmd.undo(); 31 | } 32 | } 33 | 34 | canRedo() { 35 | return this.redos.length > 0; 36 | } 37 | 38 | redo() { 39 | if (this.canRedo()) { 40 | let cmd = this.redos.pop(); 41 | this.undos.push(cmd); 42 | cmd.execute(); 43 | } 44 | } 45 | 46 | record(cmd: Command) { 47 | if (this.undos.length === this.limit) this.undos.shift(); 48 | this.undos.push(cmd); 49 | this.redos = []; 50 | } 51 | 52 | execute(cmd: Command,toHistory:boolean=true) { 53 | if(toHistory){ 54 | this.record(cmd); 55 | } 56 | cmd.execute(); 57 | } 58 | 59 | // private onKeyDown( 60 | // e: Event & { 61 | // key: string 62 | // metaKey: boolean 63 | // ctrlKey: boolean 64 | // shiftKey: boolean 65 | // } 66 | // ) { 67 | 68 | // if (this.settings?.history?.keyboard?.enabled === false) { 69 | // return 70 | // } 71 | 72 | // const isSpecialKey = e.metaKey || e.ctrlKey 73 | // const isShiftKey = e.shiftKey === true 74 | // const key = e.key.toLowerCase() 75 | 76 | // isSpecialKey && !isShiftKey && key === 'z' && this.undo() 77 | // isSpecialKey && isShiftKey && key === 'z' && this.redo() 78 | // } 79 | 80 | } 81 | 82 | export default UndoRedoManager; -------------------------------------------------------------------------------- /src/constants/EventType.ts: -------------------------------------------------------------------------------- 1 | let EVENT_TYPE = { 2 | ADD_KONVA_NODE:'addKonvaNode', 3 | ADD_NODES:'addNodes', 4 | ADD_KONVA_NODES:'addKonvaNodes', 5 | 6 | REMOVE_NODE: 'removeNode', 7 | REMOVE_NODES: 'removeNodes', 8 | WRAP_NODES_INTO_GROUP: 'wrapNodesIntoGroup', 9 | UNWRAP_NODES_FROM_GROUP: 'unwrapNodesFromGroup', 10 | SELECT_CHANGE: 'selectionChange', 11 | FN_STATE_CHANGE: 'fnStateChange', 12 | PROPERTY_CHANGE:'propertyChange', 13 | SELECTION_KONVA_NODE_CHANGE:'selectionKonvaNodeChange', 14 | NODE_ATTRIBUTE_CHANGE: 'nodeAttributeChange', 15 | NODE_EVENTS_CHANGE:'nodeEventsChange', 16 | NODE_PROPERTY_CHANGE:'nodePropertyChange', 17 | //节点的变量发生改变 18 | NODE_VARIABLE_CHANGE:'nodeVariableChange', 19 | MODEL_VARIABLE_CHANGE:'modelVariableChange', 20 | NODE_ANIMATION_CHANGE:'nodeAnimationChange', 21 | BACKGROUND_COLOR_CHANGE:'bgColorChange', 22 | ZINDEX_CHANGE:'zIndexChange' 23 | }; 24 | 25 | export default EVENT_TYPE; -------------------------------------------------------------------------------- /src/constants/StyleMap.ts: -------------------------------------------------------------------------------- 1 | const RESIZE_STYLE = ['x', 'y', 'scaleX', 'scaleY', 'skewX', 'skewY', 'rotation', 'points']; 2 | const MOVE_STYLE: any = ['x', 'y']; 3 | const STYLE_DEFAULT_VAL: any = { 'scaleX': 1, 'scaleY': 1, 'skewX': 0, 'skewY': 0, 'rotation': 0 }; 4 | const ACTION_TO_STYLE_MAP: any = { 5 | 'move': MOVE_STYLE, 6 | 'resize': RESIZE_STYLE, 7 | 'rotate': ['x', 'y', 'rotation'], 8 | 'lineResize': ['x1', 'x2', 'y1', 'y2'] 9 | }; 10 | 11 | export { RESIZE_STYLE, MOVE_STYLE, STYLE_DEFAULT_VAL, ACTION_TO_STYLE_MAP }; -------------------------------------------------------------------------------- /src/constants/TemcConstants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const SCRIPT = 'script'; 3 | export const COMPARISON = 'operation'; 4 | 5 | 6 | export const ROTATE_BY_CENTER = 'rotateByCenter'; 7 | export const BLINK = 'blink'; 8 | export const FLOW = 'flow'; 9 | export const NONE = 'none'; 10 | 11 | export const TRANSFORM = 'transform'; 12 | 13 | 14 | //事件行为 15 | export const UPDATE_ANIMATION_ACTION = "updateAnimation"; 16 | export const CHANGE_ATTRS_ACTION = 'changeAttributes'; 17 | export const EXECUTE_SCRIPT_ACTION = 'executeScript'; 18 | //事件类型 19 | export const MOUSE_LEAVE_EVT_TYPE = 'mouseleave'; 20 | export const MOUSE_ENTER_EVT_TYPE = 'mouseenter'; 21 | export const MOUSE_CLICK_EVT_TYPE = 'click'; 22 | export const MOUSE_DBL_CLICK_EVT_TYPE = 'dblclick'; 23 | export const VALUE_UPDATE_EVT_TYPE = 'valuechange'; 24 | 25 | export const EVENT_TRIGGERS = 'triggers'; 26 | 27 | 28 | export const DIRECTION_LEFT = 'left'; 29 | export const DIRECTION_RIGHT = 'right'; 30 | export const DIRECTION_TOP = 'top'; 31 | export const DIRECTION_BOTTOM = 'bottom'; 32 | export const DIRECTION_HORIZONTAL = 'horizontal'; 33 | export const DIRECTION_VERTICAL = 'vertical'; 34 | 35 | export const REGULAR_MODE = 'regularMode'; 36 | export const DRAWING_MODE = 'drawingMode'; 37 | export const EDITING_MODE = 'editingMode'; 38 | export const DRAWING_MODE_SUB_CONNECTED_LINE ='drawingModeSubConnectedLine' 39 | 40 | export const COVER_FIT = 'cover'; 41 | export const CONTAIN_FIT = 'contain'; 42 | export const NORMAL_FIT = 'normal'; 43 | export const LEFT_ALIGN ='left'; 44 | export const RIGHT_ALIGN ='right'; 45 | export const CENTER_ALIGN ='center'; 46 | export const TOP_ALIGN ='top'; 47 | export const BOTTOM_ALIGN ='bottom'; 48 | 49 | 50 | export const DRAWING_MOUSE_DOWN = 0; 51 | export const DRAWING_MOUSE_MOVE = 1; 52 | export const DRAWING_MOUSE_UP = 2; 53 | export const DRAWING_MOUSE_DBL_CLICK = 3; 54 | export const DRAWING_MOUSE_CLICK=4; 55 | export const DRAWING_MOUSE_OUT=5; 56 | 57 | export const MAX_SCALE = 10; 58 | export const MIN_SCALE = 0.1; 59 | 60 | export const GRAPH_EDITOR_WARNING = "GraphEditor warning "; 61 | export const GRAPH_EDITOR_INFO = "GraphEditor info "; 62 | 63 | 64 | const supportAnimation = [BLINK, ROTATE_BY_CENTER, FLOW,NONE]; 65 | const supportEventType = [MOUSE_CLICK_EVT_TYPE, MOUSE_LEAVE_EVT_TYPE, MOUSE_ENTER_EVT_TYPE,MOUSE_DBL_CLICK_EVT_TYPE, VALUE_UPDATE_EVT_TYPE]; 66 | const supportEventAction = [UPDATE_ANIMATION_ACTION, CHANGE_ATTRS_ACTION, EXECUTE_SCRIPT_ACTION]; 67 | const animationToDefaultPeriod: any = { 68 | 'blink': 1, 69 | 'rotateByCenter': 6, 70 | 'flow': 4 71 | } 72 | 73 | export { 74 | supportAnimation, 75 | supportEventType, 76 | supportEventAction, 77 | animationToDefaultPeriod 78 | } -------------------------------------------------------------------------------- /src/index.all.node.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | export * from './index.all' -------------------------------------------------------------------------------- /src/index.all.ts: -------------------------------------------------------------------------------- 1 | // main entry for umd build for rollup 2 | /* 3 | * JavaScript Framework v@@version 4 | * Licensed under the MIT 5 | * Date: @@date 6 | */ 7 | import GraphEditor from './GraphEditor'; 8 | import GraphViewer from './GraphViewer'; 9 | 10 | import { SymbolNode } from './model/SymbolNode'; 11 | import { RectNode } from './model/RectNode'; 12 | import { EllipseNode } from './model/EllipseNode'; 13 | import { CircleNode } from './model/CircleNode'; 14 | import { ImageNode } from './model/ImageNode'; 15 | import { StarNode } from "./model/StarNode"; 16 | import { RegularPolygonNode } from './model/RegularPolygonNode'; 17 | import { ArcNode } from "./model/ArcNode"; 18 | import { PathNode } from "./model/PathNode"; 19 | import { TextNode } from "./model/TextNode"; 20 | import { RingNode } from './model/RingNode'; 21 | import { WedgeNode } from './model/WedgeNode'; 22 | import { LabelNode } from "./model/LabelNode"; 23 | import { GroupNode } from "./model/GroupNode"; 24 | import { LineArrowNode } from "./model/LineArrowNode"; 25 | import { LineNode } from "./model/LineNode"; 26 | import { PenNode } from "./model/PenNode"; 27 | import { PolylineNode } from "./model/PolylineNode"; 28 | import { PolylineArrowNode } from "./model/PolylineArrowNode"; 29 | import { Utils } from './Utils'; 30 | 31 | import { StraightConnectedLineNode } from './model/StraightConnectedLineNode'; 32 | 33 | 34 | 35 | RectNode.register(); 36 | EllipseNode.register(); 37 | ArcNode.register(); 38 | CircleNode.register(); 39 | GroupNode.register(); 40 | ImageNode.register(); 41 | LabelNode.register(); 42 | LineArrowNode.register(); 43 | LineNode.register(); 44 | PathNode.register(); 45 | PenNode.register(); 46 | PolylineArrowNode.register(); 47 | PolylineNode.register() 48 | RegularPolygonNode.register(); 49 | RingNode.register(); 50 | StarNode.register(); 51 | SymbolNode.register(); 52 | TextNode.register(); 53 | WedgeNode.register(); 54 | StraightConnectedLineNode.register(); 55 | 56 | export { GraphEditor } 57 | export { GraphViewer } 58 | 59 | 60 | export { DataModel } from './DataModel'; 61 | export { Node } from './model/Node'; 62 | export { RectNode } from './model/RectNode'; 63 | export { EllipseNode } from './model/EllipseNode'; 64 | export { GroupNode } from './model/GroupNode'; 65 | export { ImageNode } from './model/ImageNode'; 66 | export { LabelNode } from './model/LabelNode'; 67 | export { LineArrowNode } from './model/LineArrowNode'; 68 | export { LineNode } from './model/LineNode'; 69 | export { PathNode } from './model/PathNode'; 70 | export { PenNode } from './model/PenNode'; 71 | export { PolylineNode } from './model/PolylineNode'; 72 | export { RegularPolygonNode } from './model/RegularPolygonNode'; 73 | export { RingNode } from './model/RingNode'; 74 | export { ShapeNode } from './model/ShapeNode'; 75 | export { StarNode } from './model/StarNode'; 76 | export { SymbolNode } from './model/SymbolNode'; 77 | export { TextNode } from './model/TextNode'; 78 | export { WedgeNode } from './model/WedgeNode'; 79 | export { EditableShapeNode } from './model/EditableShapeNode'; 80 | export {EditorConfig} from './types/config'; 81 | export {GridConfig} from './types/config'; 82 | export {StyleConfig} from './types/config'; 83 | export {AlignDirection} from './types/common' 84 | export {GEvent,EventType,EventAction,EventWhenType} from './types/common' 85 | 86 | export {Utils}; 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/lib/GifuctLib.ts: -------------------------------------------------------------------------------- 1 | import Konva from "konva"; 2 | import { parseGIF, decompressFrames, ParsedFrame } from "gifuct-js"; 3 | export class GifuctLib { 4 | tempCanvas: any; 5 | tempCtx: any; 6 | c: any; 7 | ctx: any; 8 | gifCanvas: any; 9 | gifCtx: any; 10 | loadedFrames:any; 11 | frameIndex:number=1; 12 | pixelPercent = 100; 13 | frameImageData: ImageData; 14 | playing:any=true; 15 | async loadGIF(gifURL: string) { 16 | const resp = await fetch(gifURL); 17 | const arrayBuffer = await resp.arrayBuffer(); 18 | const gif = parseGIF(arrayBuffer); 19 | const frames = decompressFrames(gif, true); 20 | return { gif, frames }; 21 | }; 22 | 23 | renderGIF (frames: ParsedFrame[]) { 24 | this.loadedFrames = frames; 25 | this.frameIndex = 0; 26 | this.c = document.createElement("canvas"); 27 | this.ctx = this.c.getContext("2d"); 28 | this.c.width = frames[0].dims.width; 29 | this.c.height = frames[0].dims.height; 30 | // full gif canvas 31 | this.gifCanvas = document.createElement("canvas"); 32 | this.gifCtx = this.gifCanvas.getContext("2d"); 33 | this.gifCanvas.width = this.c.width; 34 | this.gifCanvas.height = this.c.height; 35 | this.tempCanvas = document.createElement("canvas") 36 | this.tempCtx = this.tempCanvas.getContext("2d") 37 | }; 38 | 39 | drawPatch (frame: ParsedFrame) { 40 | 41 | if (this.gifCtx && this.tempCtx) { 42 | const dims = frame.dims; 43 | 44 | if ( 45 | !this.frameImageData || 46 | dims.width != this.frameImageData.width || 47 | dims.height != this.frameImageData.height 48 | ) { 49 | this.tempCanvas.width = dims.width; 50 | this.tempCanvas.height = dims.height; 51 | this.frameImageData = this.tempCtx.createImageData(dims.width, dims.height); 52 | } 53 | 54 | // set the patch data as an override 55 | this.frameImageData.data.set(frame.patch); 56 | 57 | // draw the patch back over the canvas 58 | this.tempCtx.putImageData(this.frameImageData, 0, 0); 59 | 60 | this.gifCtx.drawImage(this.tempCanvas, dims.left, dims.top); 61 | } else { 62 | console.error("drawPatch error ctx not defined", this.ctx, this.gifCtx); 63 | } 64 | }; 65 | 66 | manipulate() { 67 | if (this.ctx && this.gifCtx) { 68 | const imageData =this.gifCtx.getImageData( 69 | 0, 70 | 0, 71 | this.gifCanvas.width, 72 | this.gifCanvas.height 73 | ); 74 | const other = this.gifCtx.createImageData(this.gifCanvas.width, this.gifCanvas.height); 75 | 76 | 77 | // do pixelation 78 | const pixelsX = 5 + Math.floor((this.pixelPercent / 100) * (this.c.width - 5)); 79 | const pixelsY = (pixelsX * this.c.height) / this.c.width; 80 | 81 | if (this.pixelPercent != 100) { 82 | // ctx.mozImageSmoothingEnabled = false 83 | // ctx.webkitImageSmoothingEnabled = false 84 | this.ctx.imageSmoothingEnabled = false; 85 | } 86 | 87 | this.ctx.putImageData(imageData, 0, 0); 88 | this.ctx.drawImage(this.c, 0, 0, this.c.width, this.c.height, 0, 0, pixelsX, pixelsY); 89 | this.ctx.drawImage(this.c, 0, 0, pixelsX, pixelsY, 0, 0, this.c.width, this.c.height); 90 | } else { 91 | console.error("manipulate error ctx not defined", this.ctx, this.gifCtx); 92 | } 93 | }; 94 | 95 | renderFrame (layer: Konva.Layer){ 96 | let _this=this; 97 | // get the frame 98 | const frame = this.loadedFrames[this.frameIndex]; 99 | 100 | const start = new Date().getTime(); 101 | 102 | if (frame.disposalType === 2 && this.gifCtx) { 103 | // gifCtx.clearRect(0, 0, c.width, c.height) 104 | // see: https://github.com/matt-way/gifuct-js/issues/35 105 | const prevFrame = this.loadedFrames[this.frameIndex - 1]; 106 | const { width, height, left, top } = prevFrame.dims; 107 | this.gifCtx.clearRect(left, top, width, height); 108 | } 109 | 110 | // draw the patch 111 | this.drawPatch(frame); 112 | 113 | // perform manipulation 114 | this.manipulate(); 115 | 116 | // update the frame index 117 | this.frameIndex++; 118 | if (this.frameIndex >=this.loadedFrames.length) { 119 | this.frameIndex = 0; 120 | } 121 | 122 | const end = new Date().getTime(); 123 | const diff = end - start; 124 | 125 | layer.draw(); 126 | 127 | if (this.playing) { 128 | // delay the next gif frame 129 | setTimeout(function () { 130 | requestAnimationFrame(() => _this.renderFrame(layer)); 131 | }, Math.max(0, Math.floor(frame.delay - diff))); 132 | } 133 | }; 134 | 135 | destoryAnimation(){ 136 | this.playing=false; 137 | } 138 | 139 | async load(gifURL: string, layer: Konva.Layer, callbackFn: any) { 140 | let _this = this; 141 | try { 142 | const { gif, frames } = await _this.loadGIF(gifURL); 143 | 144 | _this.renderGIF(frames); 145 | _this.renderFrame(layer); 146 | callbackFn(this.c); 147 | return this.c; 148 | } catch (error) { 149 | console.log("gifuct try/catch", error); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/lib/gifuct.ts: -------------------------------------------------------------------------------- 1 | import { parseGIF, decompressFrames, ParsedFrame } from "gifuct-js"; 2 | import Konva from "konva"; 3 | 4 | // user canvas 5 | 6 | 7 | // gif patch canvas 8 | let tempCanvas:any; 9 | let tempCtx:any ; 10 | let c:any,ctx:any,gifCanvas:any,gifCtx:any; 11 | 12 | 13 | /** 14 | * load a gif with the current input url value 15 | * 16 | */ 17 | const loadGIF = async (gifURL: string) => { 18 | const resp = await fetch(gifURL); 19 | const arrayBuffer = await resp.arrayBuffer(); 20 | const gif = parseGIF(arrayBuffer); 21 | const frames = decompressFrames(gif, true); 22 | return { gif, frames }; 23 | }; 24 | 25 | let loadedFrames: ParsedFrame[] = []; 26 | let frameIndex = 1; 27 | let playing = true; 28 | 29 | const pixelPercent = 100; 30 | 31 | let frameImageData: ImageData; 32 | 33 | const renderGIF = (frames: ParsedFrame[]) => { 34 | loadedFrames = frames; 35 | frameIndex = 0; 36 | c = document.createElement("canvas"); 37 | ctx = c.getContext("2d"); 38 | c.width = frames[0].dims.width; 39 | c.height = frames[0].dims.height; 40 | // full gif canvas 41 | gifCanvas = document.createElement("canvas"); 42 | gifCtx = gifCanvas.getContext("2d"); 43 | gifCanvas.width = c.width; 44 | gifCanvas.height = c.height; 45 | tempCanvas = document.createElement("canvas") 46 | tempCtx = tempCanvas.getContext("2d") 47 | 48 | 49 | }; 50 | 51 | const drawPatch = (frame: ParsedFrame) => { 52 | 53 | if (gifCtx && tempCtx) { 54 | const dims = frame.dims; 55 | 56 | if ( 57 | !frameImageData || 58 | dims.width != frameImageData.width || 59 | dims.height != frameImageData.height 60 | ) { 61 | tempCanvas.width = dims.width; 62 | tempCanvas.height = dims.height; 63 | frameImageData = tempCtx.createImageData(dims.width, dims.height); 64 | } 65 | 66 | // set the patch data as an override 67 | frameImageData.data.set(frame.patch); 68 | 69 | // draw the patch back over the canvas 70 | tempCtx.putImageData(frameImageData, 0, 0); 71 | 72 | gifCtx.drawImage(tempCanvas, dims.left, dims.top); 73 | } else { 74 | console.error("drawPatch error ctx not defined", ctx, gifCtx); 75 | } 76 | }; 77 | 78 | const manipulate = () => { 79 | if (ctx && gifCtx) { 80 | const imageData = gifCtx.getImageData( 81 | 0, 82 | 0, 83 | gifCanvas.width, 84 | gifCanvas.height 85 | ); 86 | const other = gifCtx.createImageData(gifCanvas.width, gifCanvas.height); 87 | 88 | 89 | // do pixelation 90 | const pixelsX = 5 + Math.floor((pixelPercent / 100) * (c.width - 5)); 91 | const pixelsY = (pixelsX * c.height) / c.width; 92 | 93 | if (pixelPercent != 100) { 94 | // ctx.mozImageSmoothingEnabled = false 95 | // ctx.webkitImageSmoothingEnabled = false 96 | ctx.imageSmoothingEnabled = false; 97 | } 98 | 99 | ctx.putImageData(imageData, 0, 0); 100 | ctx.drawImage(c, 0, 0, c.width, c.height, 0, 0, pixelsX, pixelsY); 101 | ctx.drawImage(c, 0, 0, pixelsX, pixelsY, 0, 0, c.width, c.height); 102 | } else { 103 | console.error("manipulate error ctx not defined", ctx, gifCtx); 104 | } 105 | }; 106 | 107 | const renderFrame = (layer: Konva.Layer) => { 108 | // get the frame 109 | const frame = loadedFrames[frameIndex]; 110 | 111 | const start = new Date().getTime(); 112 | 113 | if (frame.disposalType === 2 && gifCtx) { 114 | // gifCtx.clearRect(0, 0, c.width, c.height) 115 | // see: https://github.com/matt-way/gifuct-js/issues/35 116 | const prevFrame = loadedFrames[frameIndex - 1]; 117 | const { width, height, left, top } = prevFrame.dims; 118 | gifCtx.clearRect(left, top, width, height); 119 | } 120 | 121 | // draw the patch 122 | drawPatch(frame); 123 | 124 | // perform manipulation 125 | manipulate(); 126 | 127 | // update the frame index 128 | frameIndex++; 129 | if (frameIndex >= loadedFrames.length) { 130 | frameIndex = 0; 131 | } 132 | 133 | const end = new Date().getTime(); 134 | const diff = end - start; 135 | 136 | layer.draw(); 137 | 138 | if (playing) { 139 | // delay the next gif frame 140 | setTimeout(function () { 141 | requestAnimationFrame(() => renderFrame(layer)); 142 | }, Math.max(0, Math.floor(frame.delay - diff))); 143 | } 144 | }; 145 | 146 | const gifuct = async (gifURL: string, layer: Konva.Layer, callbackFn: any) => { 147 | try { 148 | const { gif, frames } = await loadGIF(gifURL); 149 | 150 | renderGIF(frames); 151 | renderFrame(layer); 152 | callbackFn(c); 153 | return c; 154 | } catch (error) { 155 | console.log("gifuct try/catch", error); 156 | } 157 | }; 158 | 159 | export default gifuct; 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/model/ArcNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class ArcNodeAttrs extends ShapeNodeAttrs { 6 | angle = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 7 | innerRadius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 8 | outerRadius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 9 | clockwise = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 10 | } 11 | 12 | export class ArcNode extends ShapeNode { 13 | 14 | static className = 'ArcNode'; 15 | className:string='ArcNode'; 16 | attributes = new ArcNodeAttrs(); 17 | 18 | constructor(opt?: any) { 19 | super(); 20 | 21 | if (opt) this.fromObject(opt); 22 | } 23 | 24 | createRef() { 25 | let konvaNode = new Konva.Arc(); 26 | konvaNode.id(this.id); 27 | this.ref = konvaNode; 28 | } 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/model/BaseConnectedLineNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | import { EditableShapeNode, EditableShapeNodeAttrs } from './EditableShapeNode'; 5 | 6 | export class BaseConnectedLineNodeAttrs extends EditableShapeNodeAttrs { 7 | points = { "value": [], "default": [], "group": "geometry", "type": "Array" }; 8 | } 9 | 10 | export class BaseConnectedLineNode extends EditableShapeNode { 11 | from: any; 12 | to: any; 13 | 14 | setFrom(from: string) { 15 | this.from = from; 16 | } 17 | setTo(to: string) { 18 | this.to = to; 19 | } 20 | updateRefAnchors(attrValues: any): void { 21 | 22 | } 23 | createRef(): void { 24 | let konvaNode = new Konva.Line(); 25 | konvaNode.id(this.id); 26 | this.ref = konvaNode; 27 | } 28 | 29 | 30 | attributes = new BaseConnectedLineNodeAttrs(); 31 | 32 | constructor(opt?: any) { 33 | super(); 34 | if (opt) this.fromObject(opt); 35 | } 36 | 37 | /** 38 | * 将节点转成JSON对象 39 | * @param isArray 是否去掉key,转成数组,节省空间 40 | * @param distinct 是否过滤掉属性值为默认值的属性 41 | * @param unique 是否id唯一,如果为true,则会重新生成id 42 | * @returns 43 | */ 44 | toObject(isArray: boolean = false, distinct: boolean = true, unique: boolean = false) { 45 | let obj: any = super.toObject(isArray, distinct, unique); 46 | 47 | obj.from = this.from; 48 | obj.to = this.to; 49 | return obj; 50 | } 51 | 52 | fromObject(obj: any) { 53 | super.fromObject(obj); 54 | this.setFrom(obj.from); 55 | this.setTo(obj.to); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/model/CircleNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class CircleNodeAttrs extends ShapeNodeAttrs { 6 | radius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 7 | } 8 | 9 | export class CircleNode extends ShapeNode { 10 | 11 | static className = 'CircleNode'; 12 | className = 'CircleNode'; 13 | attributes = new CircleNodeAttrs(); 14 | 15 | constructor(opt?: any) { 16 | super(); 17 | if (opt) this.fromObject(opt); 18 | } 19 | 20 | createRef() { 21 | let konvaNode = new Konva.Circle(); 22 | konvaNode.id(this.id); 23 | this.ref = konvaNode; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/model/ContainerNode.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import { Node, NodeAttrs } from './Node'; 3 | import {Utils} from '../Utils'; 4 | 5 | export class ContainerNodeAttrs extends NodeAttrs { } 6 | 7 | export abstract class ContainerNode extends Node { 8 | 9 | className = 'ContainerNode'; 10 | members: Array = []; 11 | 12 | getMembers() { 13 | return this.members; 14 | } 15 | 16 | setMembers(nodes: Array) { 17 | this.members = nodes; 18 | this.ref.destroyChildren(); 19 | this.ref.add(...nodes.map(node => node.getRef())); 20 | } 21 | 22 | getMemberAttributes(unionMode: boolean = false) { 23 | let memberAttrs: any = {}, members = this.members; 24 | for (let i = 0, len = members.length; i < len; i++) { 25 | let member = members[i], attrs = member instanceof ContainerNode ? member.getMemberAttributes(unionMode) : member.getAttributes(true); 26 | if (i === 0) { 27 | memberAttrs = attrs; 28 | } else if (unionMode) { 29 | Object.assign(memberAttrs, attrs); 30 | } else { 31 | let loop = false; 32 | for (let name in memberAttrs) { 33 | loop = true; 34 | if (!attrs.hasOwnProperty(name)) { 35 | delete memberAttrs[name]; 36 | } 37 | } 38 | if (!loop) break; 39 | } 40 | } 41 | return memberAttrs; 42 | } 43 | 44 | setMemberAttributeValues(attrValues: any) { 45 | this.members.forEach(member => { 46 | if (member instanceof ContainerNode) { 47 | member.setMemberAttributeValues(attrValues); 48 | } else { 49 | member.setAttributeValues(attrValues); 50 | } 51 | }); 52 | } 53 | 54 | fromObject(obj: any) { 55 | super.fromObject(obj); 56 | let members; 57 | if (obj instanceof Array) { 58 | members = obj[7]; 59 | } else { 60 | ({ members } = obj); 61 | } 62 | if (members) this.setMembers(members.map((item: any) => Node.create(item))); 63 | } 64 | 65 | toObject(isArray: boolean = false, distinct: boolean = true,unique:boolean=false) { 66 | let obj: any = super.toObject(isArray,distinct,unique); 67 | let members = this.getMembers().map((item: any) => item.toObject(isArray,distinct,unique)); 68 | if (isArray) { 69 | obj.push(members); 70 | } else { 71 | obj.members = members; 72 | } 73 | return obj; 74 | } 75 | 76 | createRef() { 77 | let konvaNode = new Konva.Group(); 78 | konvaNode.id(this.id); 79 | this.ref = konvaNode; 80 | } 81 | updateRefAttrs(attrValues: any) { 82 | super.updateRefAttrs(attrValues); 83 | (function loop(nodes) { 84 | for (let node of nodes) { 85 | if (node instanceof ContainerNode) { 86 | loop(node.getMembers()); 87 | } else { 88 | //如果有动画是正在执行的,则要重新生成动画 89 | let shouldUpdateAnimation = Utils.getShouldUpdateAnimation(attrValues); 90 | let autoPlay = node.getAnimationValue('autoPlay'); 91 | if (autoPlay && shouldUpdateAnimation) { 92 | node.updateRefAnimation("updateRefAttrs"); 93 | } 94 | } 95 | } 96 | })(this.getMembers()); 97 | } 98 | 99 | /** 100 | * 更新konva节点的动画 101 | */ 102 | updateRefAnimation(reason: string) { 103 | super.updateRefAnimation(reason); 104 | (function loop(nodes) { 105 | for (let node of nodes) { 106 | if (node instanceof ContainerNode) { 107 | loop(node.getMembers()); 108 | } else { 109 | node.updateRefAnimation(reason); 110 | } 111 | } 112 | })(this.getMembers()); 113 | } 114 | 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/model/EditableShapeNode.ts: -------------------------------------------------------------------------------- 1 | import {Utils} from '../Utils'; 2 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 3 | 4 | export abstract class EditableShapeNodeAttrs extends ShapeNodeAttrs { } 5 | 6 | export abstract class EditableShapeNode extends ShapeNode { 7 | 8 | className = 'EditableShapeNode'; 9 | refAnchors: any = null; 10 | 11 | getRefAnchors() { 12 | return this.refAnchors; 13 | } 14 | 15 | createRefAnchors() { 16 | 17 | } 18 | 19 | abstract updateRefAnchors(attrValues: any): void; 20 | 21 | destroyRefAnchors() { 22 | if (this.refAnchors !== null) { 23 | let anchors = this.refAnchors; 24 | for (let anchor of anchors) anchor.destroy(); 25 | this.refAnchors = null; 26 | } 27 | } 28 | 29 | setAttributeValues(attrValues: any) { 30 | super.setAttributeValues(attrValues); 31 | this.updateRefAnchors(attrValues); 32 | } 33 | 34 | remove() { 35 | let index = this.getZIndex(); 36 | if (index !== -1) { 37 | this.dataModel.nodes.splice(index, 1); 38 | if (this.ref !== null) this.ref.remove(); 39 | this.destroyRefAnchors(); 40 | } 41 | } 42 | destroy(){ 43 | let index = this.getZIndex(); 44 | if (index !== -1) { 45 | this.dataModel.nodes.splice(index, 1); 46 | if (this.ref !== null) this.ref.destroy(); 47 | this.destroyRefAnchors(); 48 | } 49 | } 50 | getAnchorRadius(strokeWidth:number){ 51 | 52 | let scale=this.ref.getParent().getStage().getAttr('scaleX'); 53 | return 3/scale; 54 | // if(strokeWidth<3){ 55 | 56 | // }else{ 57 | // return strokeWidth/scale; 58 | // } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/model/EllipseNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class EllipseNodeAttrs extends ShapeNodeAttrs { 6 | radiusX = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 7 | radiusY = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 8 | } 9 | 10 | export class EllipseNode extends ShapeNode { 11 | 12 | static className = 'EllipseNode'; 13 | className = 'EllipseNode'; 14 | attributes = new EllipseNodeAttrs(); 15 | 16 | constructor(opt?: any) { 17 | super(); 18 | if (opt) this.fromObject(opt); 19 | } 20 | 21 | createRef() { 22 | let konvaNode = new Konva.Ellipse(); 23 | konvaNode.id(this.id); 24 | this.ref = konvaNode; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/model/GroupNode.ts: -------------------------------------------------------------------------------- 1 | import { ContainerNode, ContainerNodeAttrs } from './ContainerNode'; 2 | 3 | export class GroupNodeAttrs extends ContainerNodeAttrs { } 4 | 5 | export class GroupNode extends ContainerNode { 6 | 7 | static className = 'GroupNode'; 8 | className = 'GroupNode'; 9 | attributes = new GroupNodeAttrs(); 10 | 11 | constructor(opt?: any) { 12 | super(); 13 | if (opt) this.fromObject(opt); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/model/ImageNode.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 3 | import { Utils } from '../Utils'; 4 | import gifuct from '../lib/gifuct'; 5 | import { GRAPH_EDITOR_WARNING } from '../constants/TemcConstants'; 6 | import { GifuctLib } from '../lib/GifuctLib'; 7 | 8 | 9 | export class ImageNodeAttrs extends ShapeNodeAttrs { 10 | image = { "value": undefined, "default": undefined, "group": "geometry", "type": "Image" }; 11 | crop = { "value": null, "default": null, "group": "geometry", "type": "Object" }; 12 | width = { "value": "auto", "default": "auto", "group": "geometry", "type": "Number" }; 13 | height = { "value": "auto", "default": "auto", "group": "geometry", "type": "Number" }; 14 | name = { "value": "", "default": "", "group": "", "type": "String" }; 15 | 16 | } 17 | 18 | export class ImageNode extends ShapeNode { 19 | 20 | 21 | static className = 'ImageNode'; 22 | className = 'ImageNode'; 23 | attributes = new ImageNodeAttrs(); 24 | gifuctLib:any; 25 | constructor(opt?: any) { 26 | super(); 27 | if (opt) this.fromObject(opt); 28 | } 29 | setAttributeValues(attrValues: any) { 30 | 31 | let _this = this; 32 | super.setAttributeValues(attrValues); 33 | let { image } = attrValues; 34 | if (image && image != "undefined") { 35 | if (!(image.startsWith('data'))) { 36 | fetch(image) 37 | .then(response => response.blob()) 38 | .then(blob => { 39 | const reader = new FileReader() 40 | reader.readAsDataURL(blob) 41 | reader.onloadend = () => { 42 | const base64data = reader.result?.toString(); 43 | if (base64data) { 44 | _this.setAttributeValue('image', base64data); 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | } 51 | 52 | 53 | 54 | updateRefAttrs(attrValues: any) { 55 | if (this.ref !== null) { 56 | this.ref.setAttrs(attrValues); 57 | let { image } = attrValues; 58 | if (image && image != "undefined") { 59 | if (image.startsWith('data:image/gif')) { 60 | if (this.ref.getParent()) { 61 | this.gifuctLib = new GifuctLib(); 62 | let name = this.getAttributeValue('name'); 63 | this.gifuctLib.load(image, this.ref.getParent(), (c: any) => { 64 | c.setAttribute('id', name); 65 | this.ref.setAttr('image', c); 66 | }) 67 | let imageObj = new Image(); 68 | imageObj.src = image; 69 | this.ref.setAttr('image', imageObj); 70 | } 71 | 72 | } else { 73 | let imageObj = new Image(); 74 | imageObj.src = image; 75 | this.ref.setAttr('image', imageObj); 76 | } 77 | 78 | 79 | } 80 | //如果有动画是正在执行的,则要根据属性的改变重新生成动画,比如修改了位置,则旋转动画要重新生成 81 | let shouldUpdateAnimation = Utils.getShouldUpdateAnimation(attrValues); 82 | let autoPlay = this.getAnimationValue('autoPlay'); 83 | let isRotate = this.getAnimationValue('type') == 'rotateByCenter'; 84 | if (autoPlay && isRotate && shouldUpdateAnimation) { 85 | this.updateRefAnimation("updateRefAttrs"); 86 | } 87 | } 88 | } 89 | updateGifAnimation(reason: string) { 90 | let image = this.getAttributeValue('image'); 91 | let name = this.getAttributeValue('name'); 92 | if (image.startsWith('data:image/gif')) { 93 | if (this.ref.getParent()) { 94 | this.gifuctLib = new GifuctLib(); 95 | this.gifuctLib.load(image, this.ref.getParent(), (c: any) => { 96 | c.setAttribute('id', name); 97 | this.ref.setAttr('image', c); 98 | }) 99 | let imageObj = new Image(); 100 | imageObj.src = image; 101 | this.ref.setAttr('image', imageObj); 102 | } else { 103 | console.warn(GRAPH_EDITOR_WARNING + "节点未加入到Layer,不能渲染gif") 104 | } 105 | 106 | } 107 | } 108 | 109 | createRef() { 110 | let konvaNode = new Konva.Image({ image: undefined }); 111 | konvaNode.id(this.id); 112 | this.ref = konvaNode; 113 | } 114 | 115 | remove() { 116 | super.remove() 117 | this.destroyGifAnimation(); 118 | } 119 | destroyGifAnimation(){ 120 | if(this.gifuctLib){ 121 | this.gifuctLib.destoryAnimation() 122 | this.gifuctLib=null; 123 | } 124 | } 125 | /** 126 | * 销毁节点 127 | */ 128 | destroy() { 129 | super.remove() 130 | this.destroyGifAnimation(); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/model/LabelNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class LabelNodeAttrs extends ShapeNodeAttrs { 6 | textFill = { "value": "", "default": "", "group": "fill", "type": "String" }; 7 | fontFamily = { "value": "Arial", "default": "Arial", "group": "font", "type": "String", enums: ["宋体", "Arial", "Calibri"] }; 8 | fontSize = { "value": 12, "default": 12, "group": "font", "type": "Number" }; 9 | fontStyle = { "value": "normal", "default": "normal", "group": "font", "type": "String", enums: ["normal", "bold", "italic", "bold italic"] }; 10 | fontVariant = { "value": "normal", "default": "normal", "group": "font", "type": "String", enums: ["normal", "small-caps"] }; 11 | textDecoration = { "value": "", "default": "", "group": "font", "type": "String", enums: ["", "underline", "line-through", "underline line-through"] }; 12 | text = { "value": "", "default": "", "group": "text", "type": "String" }; 13 | pointerDirection = { "value": "", "default": "", "group": "other", "type": "String", enums: ["up", "down", "left", "right"] }; 14 | pointerWidth = { "value": 0, "default": 0, "group": "other", "type": "Number" }; 15 | pointerHeight = { "value": 0, "default": 0, "group": "other", "type": "Number" }; 16 | cornerRadius = { "value": 0, "default": 0, "group": "other", "type": "Number" }; 17 | padding = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 18 | } 19 | 20 | 21 | export class LabelNode extends ShapeNode { 22 | 23 | static className = 'LabelNode'; 24 | className = 'LabelNode'; 25 | attributes = new LabelNodeAttrs(); 26 | textAttributes = ['text', 'fontFamily', 'fontSize', 'padding', 'textFill', 'textDecoration', 'fontStyle']; 27 | labelAttributes = ['x', 'y', 'rotation', 'scaleX', 'scaleY', 'skewX', 'skewY', 'draggable']; 28 | 29 | constructor(opt?: any) { 30 | super(); 31 | if (opt) this.fromObject(opt); 32 | } 33 | 34 | updateRefAttrs(attrValues: any) { 35 | if (this.ref !== null) { 36 | for (let name in attrValues) { 37 | let attrValue = attrValues[name]; 38 | if (this.labelAttributes.includes(name)) { 39 | this.ref.setAttr(name, attrValue); 40 | } else if (this.textAttributes.includes(name)) { 41 | this.ref.getText().setAttr(name === 'textFill' ? 'fill' : name, attrValue); 42 | } else { 43 | this.ref.getTag().setAttr(name, attrValue); 44 | } 45 | } 46 | } 47 | } 48 | 49 | createRef() { 50 | let konvaNode = new Konva.Label(); 51 | konvaNode.add(new Konva.Tag({})); 52 | konvaNode.add(new Konva.Text({})); 53 | konvaNode.id(this.id); 54 | this.ref = konvaNode; 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/model/LineArrowNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { LineNode, LineNodeAttrs } from './LineNode'; 4 | 5 | export class LineArrowNodeAttrs extends LineNodeAttrs { 6 | pointerLength = { "value": 20, "default": 20, "group": "geometry", "type": "Number" }; 7 | pointerWidth = { "value": 20, "default": 20, "group": "geometry", "type": "Number" }; 8 | pointerAtBeginning = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 9 | pointerAtEnding = { "value": true, "default": true, "group": "geometry", "type": "Boolean" }; 10 | } 11 | 12 | export class LineArrowNode extends LineNode { 13 | 14 | static className = 'LineArrowNode'; 15 | className = 'LineArrowNode'; 16 | attributes = new LineArrowNodeAttrs(); 17 | 18 | constructor(opt?: any) { 19 | super(); 20 | if (opt) this.fromObject(opt); 21 | } 22 | 23 | createRef() { 24 | let konvaNode = new Konva.Arrow(); 25 | konvaNode.id(this.id); 26 | this.ref = konvaNode; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/model/LineNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { EditableShapeNode, EditableShapeNodeAttrs } from './EditableShapeNode'; 4 | import Command from '../command/Command'; 5 | import AttributeChange from '../command/AttributeChange'; 6 | import { Utils } from '../Utils'; 7 | 8 | export class LineNodeAttrs extends EditableShapeNodeAttrs { 9 | x1 = { "value": null, "default": null, "group": "geometry", "type": "Number" }; 10 | y1 = { "value": null, "default": null, "group": "geometry", "type": "Number" }; 11 | x2 = { "value": null, "default": null, "group": "geometry", "type": "Number" }; 12 | y2 = { "value": null, "default": null, "group": "geometry", "type": "Number" }; 13 | } 14 | 15 | export class LineNode extends EditableShapeNode { 16 | 17 | static className = 'LineNode'; 18 | className = 'LineNode'; 19 | attributes: any = new LineNodeAttrs(); 20 | 21 | constructor(opt?: any) { 22 | super(); 23 | if (opt) this.fromObject(opt); 24 | } 25 | updateRefAttrs(attrValues: any) { 26 | if (this.ref !== null) { 27 | 28 | let { x1, y1, x2, y2, ...otherAttrValues } = attrValues; 29 | if (x1 !== undefined || y1 !== undefined || x2 !== undefined || y2 !== undefined) { 30 | 31 | let points = this.ref.points().slice(); 32 | if (x1 !== undefined) points[0] = x1; 33 | if (y1 !== undefined) points[1] = y1; 34 | if (x2 !== undefined) points[2] = x2; 35 | if (y2 !== undefined) points[3] = y2; 36 | otherAttrValues['points'] = points; 37 | } 38 | super.updateRefAttrs(otherAttrValues); 39 | } 40 | } 41 | 42 | createRef() { 43 | let konvaNode = new Konva.Line(); 44 | konvaNode.id(this.id); 45 | this.ref = konvaNode; 46 | } 47 | 48 | createRefAnchors(opt?: any) { 49 | super.createRefAnchors(); 50 | let transformIn = this.ref.getTransform(); 51 | let transformOut = transformIn.copy().invert(); 52 | let strokeWidth = this.attributes['strokeWidth'].value; 53 | let radius = this.getAnchorRadius(strokeWidth); 54 | let fillColor = opt ? opt.anchorFill : '#0000ff'; 55 | let anchorStrokeColor = opt ? opt.anchorStroke : '#0000ff'; 56 | let x1 = this.attributes['x1']; 57 | let y1 = this.attributes['y1']; 58 | let x2 = this.attributes['x2']; 59 | let y2 = this.attributes['y2']; 60 | 61 | let pointA = transformIn.point({ x: x1.value, y: y1.value }); 62 | let anchorA = new Konva.Circle({ 63 | x: pointA.x, 64 | y: pointA.y, 65 | radius: radius, 66 | hitStrokeWidth: radius + 8, 67 | stroke: anchorStrokeColor, 68 | fill: fillColor, 69 | draggable: true 70 | }); 71 | 72 | let oldX1: any, oldY1: any; 73 | anchorA.on('dragstart', (e: any) => { 74 | e.cancelBubble = true; 75 | oldX1 = x1.value; 76 | oldY1 = y1.value; 77 | }); 78 | anchorA.on('dragmove', () => { 79 | let point = transformOut.point({ x: anchorA.x(), y: anchorA.y() }); 80 | x1.value = point.x; 81 | y1.value = point.y; 82 | this.ref.points([x1.value, y1.value, x2.value, y2.value]); 83 | }); 84 | anchorA.on('dragend', (e: any) => { 85 | e.cancelBubble = true; 86 | let undoRedoManager = this.dataModel.getUndoRedoManager(); 87 | let map = new Map([[this, [{ x1: oldX1, y1: oldY1 }, { x1: x1.value, y1: y1.value }]]]); 88 | let attributeChange = new AttributeChange(map, this.dataModel); 89 | let cmd = new Command([attributeChange]); 90 | undoRedoManager.record(cmd); 91 | }); 92 | 93 | let pointB = transformIn.point({ x: x2.value, y: y2.value }); 94 | let anchorB = new Konva.Circle({ 95 | x: pointB.x, 96 | y: pointB.y, 97 | radius: radius, 98 | stroke: anchorStrokeColor, 99 | hitStrokeWidth: radius + 8, 100 | fill: fillColor, 101 | draggable: true 102 | }); 103 | 104 | let oldX2: any, oldY2: any; 105 | anchorB.on('dragstart', (e: any) => { 106 | e.cancelBubble = true; 107 | oldX2 = x2.value; 108 | oldY2 = y2.value; 109 | }); 110 | anchorB.on('dragmove', () => { 111 | let point = transformOut.point({ x: anchorB.x(), y: anchorB.y() }); 112 | x2.value = point.x; 113 | y2.value = point.y; 114 | this.ref.points([x1.value, y1.value, x2.value, y2.value]); 115 | }); 116 | anchorB.on('dragend', (e: any) => { 117 | e.cancelBubble = true; 118 | let undoRedoManager = this.dataModel.getUndoRedoManager(); 119 | let map = new Map([[this, [{ x2: oldX2, y2: oldY2 }, { x2: x2.value, y2: y2.value }]]]); 120 | let attributeChange = new AttributeChange(map, this.dataModel); 121 | let cmd = new Command([attributeChange]); 122 | undoRedoManager.record(cmd); 123 | }); 124 | 125 | this.refAnchors = [anchorA, anchorB]; 126 | 127 | } 128 | 129 | updateRefAnchors(attrValues: any) { 130 | if (this.refAnchors !== null) { 131 | let { x1, y1, x2, y2 } = attrValues; 132 | if (x1 !== undefined || y1 !== undefined || x2 !== undefined || y2 !== undefined) { 133 | let transformIn = this.ref.getTransform(); 134 | if (x1 !== undefined || y1 !== undefined) { 135 | if (x1 === undefined) x1 = this.attributes['x1'].value; 136 | if (y1 === undefined) y1 = this.attributes['y1'].value; 137 | let anchorA = this.refAnchors[0]; 138 | let pointA = transformIn.point({ x: x1, y: y1 }); 139 | anchorA.setAttrs({ x: pointA.x, y: pointA.y }); 140 | } 141 | if (x2 !== undefined || y2 !== undefined) { 142 | if (x2 === undefined) x2 = this.attributes['x2'].value; 143 | if (y2 === undefined) y2 = this.attributes['y2'].value; 144 | let anchorB = this.refAnchors[1]; 145 | let pointB = transformIn.point({ x: x2, y: y2 }); 146 | anchorB.setAttrs({ x: pointB.x, y: pointB.y }); 147 | } 148 | } 149 | } 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/model/PathNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class PathNodeAttrs extends ShapeNodeAttrs { 6 | data = { "value": "", "default": "", "group": "hidden", "type": "String" }; 7 | } 8 | 9 | export class PathNode extends ShapeNode { 10 | 11 | static className = 'PathNode'; 12 | className = 'PathNode'; 13 | attributes = new PathNodeAttrs(); 14 | 15 | constructor(opt?: any) { 16 | super(); 17 | if (opt) this.fromObject(opt); 18 | } 19 | 20 | createRef() { 21 | let konvaNode = new Konva.Path(); 22 | konvaNode.id(this.id); 23 | this.ref = konvaNode; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/model/PenNode.ts: -------------------------------------------------------------------------------- 1 | import Konva from "konva"; 2 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 3 | 4 | export class PenNodeAttrs extends ShapeNodeAttrs { 5 | points = { "value": [], "default": [], "group": "geometry", "type": "Array" }; 6 | tension = { "value": 0, "default": 0, "group": "geometry", "type": "Number", "min": 0, "max": 1 }; 7 | closed = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 8 | bezier = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 9 | } 10 | 11 | export class PenNode extends ShapeNode { 12 | 13 | static className = 'PenNode'; 14 | className = 'PenNode'; 15 | attributes = new PenNodeAttrs(); 16 | 17 | constructor(opt?: any) { 18 | super(); 19 | if (opt) this.fromObject(opt); 20 | } 21 | 22 | createRef() { 23 | let konvaNode = new Konva.Line(); 24 | konvaNode.id(this.id); 25 | this.ref = konvaNode; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/model/PolylineArrowNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { PolylineNode, PolylineNodeAttrs } from './PolylineNode'; 4 | 5 | export class PolylineArrowNodeAttrs extends PolylineNodeAttrs { 6 | pointerLength = { "value": 20, "default": 20, "group": "geometry", "type": "Number" }; 7 | pointerWidth = { "value": 20, "default": 20, "group": "geometry", "type": "Number" }; 8 | pointerAtBeginning = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 9 | pointerAtEnding = { "value": true, "default": true, "group": "geometry", "type": "Boolean" }; 10 | } 11 | 12 | export class PolylineArrowNode extends PolylineNode { 13 | 14 | static className = 'PolylineArrowNode' 15 | className = 'PolylineArrowNode' 16 | attributes = new PolylineArrowNodeAttrs(); 17 | 18 | constructor(opt?: any) { 19 | super(); 20 | if (opt) this.fromObject(opt); 21 | } 22 | 23 | createRef() { 24 | let konvaNode = new Konva.Arrow(); 25 | konvaNode.id(this.id); 26 | this.ref = konvaNode; 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/model/PolylineNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { EditableShapeNode, EditableShapeNodeAttrs } from './EditableShapeNode'; 4 | import Command from '../command/Command'; 5 | import AttributeChange from '../command/AttributeChange'; 6 | 7 | export class PolylineNodeAttrs extends EditableShapeNodeAttrs { 8 | points = { "value": [], "default": [], "group": "geometry", "type": "Array" }; 9 | tension = { "value": 0, "default": 0, "group": "geometry", "type": "Number", "min": 0, "max": 1 }; 10 | closed = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 11 | bezier = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 12 | } 13 | 14 | export class PolylineNode extends EditableShapeNode { 15 | 16 | static className = 'PolylineNode'; 17 | className = 'PolylineNode'; 18 | attributes = new PolylineNodeAttrs(); 19 | 20 | constructor(opt?: any) { 21 | super(); 22 | if (opt) this.fromObject(opt); 23 | } 24 | 25 | createRef() { 26 | let konvaNode = new Konva.Line(); 27 | konvaNode.id(this.id); 28 | this.ref = konvaNode; 29 | } 30 | 31 | createRefAnchors(opt?:any) { 32 | super.createRefAnchors(); 33 | if (this.refAnchors === null) { 34 | let anchors: Array = []; 35 | let transformIn = this.ref.getTransform(); 36 | let transformOut = transformIn.copy().invert(); 37 | let strokeWidth = this.attributes['strokeWidth'].value; 38 | let radius=this.getAnchorRadius(strokeWidth); 39 | let fillColor=opt?opt.anchorFill:'#0000ff'; 40 | let anchorStrokeColor=opt?opt.anchorStroke:'#0000ff'; 41 | let coords = this.attributes['points'].value as Array; 42 | for (let i = 1, len = coords.length; i < len; i += 2) { 43 | let point = transformIn.point({ x: coords[i - 1], y: coords[i] }); 44 | let anchor = new Konva.Circle({ 45 | x: point.x, 46 | y: point.y, 47 | radius: radius, 48 | hitStrokeWidth: strokeWidth * 2 + 8, 49 | stroke:anchorStrokeColor, 50 | fill: fillColor, 51 | draggable: true, 52 | shapeType: 'anchor' 53 | }); 54 | let oldCoords: any; 55 | anchor.on('dragstart', (e: any) => { 56 | e.cancelBubble = true; 57 | oldCoords = coords.slice(); 58 | }); 59 | anchor.on('dragmove', () => { 60 | let point = transformOut.point({ x: anchor.x(), y: anchor.y() }); 61 | coords[i - 1] = point.x; 62 | coords[i] = point.y; 63 | this.ref.points(coords); 64 | }); 65 | anchor.on('dragend', (e: any) => { 66 | e.cancelBubble = true; 67 | let newCoords: any = coords.slice(); 68 | let undoRedoManager = this.dataModel.getUndoRedoManager(); 69 | let map = new Map([[this, [{ points: oldCoords }, { points: newCoords }]]]); 70 | let attributeChange = new AttributeChange(map, this.dataModel); 71 | let cmd = new Command([attributeChange]); 72 | undoRedoManager.record(cmd); 73 | }); 74 | anchors.push(anchor); 75 | } 76 | this.refAnchors = anchors; 77 | } 78 | } 79 | 80 | updateRefAnchors(attrValues: any) { 81 | if (this.refAnchors !== null) { 82 | if (attrValues.hasOwnProperty('points')) { 83 | let transformIn = this.ref.getTransform(); 84 | let coords = attrValues['points'] as Array; 85 | let anchors = this.refAnchors; 86 | for (let i = 0, len = anchors.length; i < len; i++) { 87 | let point = transformIn.point({ x: coords[i * 2], y: coords[i * 2 + 1] }); 88 | anchors[i].setAttrs({ x: point.x, y: point.y }); 89 | } 90 | } 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/model/RectNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class RectNodeAttrs extends ShapeNodeAttrs { 6 | width = { "value": "auto", "default": "auto", "group": "geometry", "type": "Number" }; 7 | height = { "value": "auto", "default": "auto", "group": "geometry", "type": "Number" }; 8 | } 9 | 10 | export class RectNode extends ShapeNode { 11 | static className: string='RectNode'; 12 | attributes = new RectNodeAttrs(); 13 | className:string='RectNode'; 14 | constructor(opt?: any) { 15 | super(); 16 | if (opt) this.fromObject(opt); 17 | } 18 | 19 | createRef() { 20 | let konvaNode = new Konva.Rect(); 21 | konvaNode.id(this.id); 22 | this.ref = konvaNode; 23 | } 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/model/RegularPolygonNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class RegularPolygonNodeAttrs extends ShapeNodeAttrs { 6 | sides = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 7 | radius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 8 | } 9 | 10 | export class RegularPolygonNode extends ShapeNode { 11 | 12 | static className = 'RegularPolygonNode'; 13 | className = 'RegularPolygonNode'; 14 | attributes = new RegularPolygonNodeAttrs(); 15 | 16 | constructor(opt?: any) { 17 | super(); 18 | if (opt) this.fromObject(opt); 19 | } 20 | 21 | createRef() { 22 | let konvaNode = new Konva.RegularPolygon(); 23 | konvaNode.id(this.id); 24 | this.ref = konvaNode; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/model/RingNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class RingNodeAttrs extends ShapeNodeAttrs { 6 | innerRadius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 7 | outerRadius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 8 | } 9 | 10 | export class RingNode extends ShapeNode { 11 | 12 | static className = 'RingNode'; 13 | className = 'RingNode'; 14 | attributes = new RingNodeAttrs(); 15 | 16 | constructor(opt?: any) { 17 | super(); 18 | if (opt) this.fromObject(opt); 19 | } 20 | 21 | createRef() { 22 | let konvaNode = new Konva.Ring(); 23 | konvaNode.id(this.id); 24 | this.ref = konvaNode; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/model/ShapeNode.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeAttrs } from './Node'; 2 | 3 | export abstract class ShapeNodeAttrs extends NodeAttrs { 4 | stroke = { "value": "#000", "default": "#000", "group": "stroke", "type": "String" }; 5 | strokeWidth = { "value": 1, "default": 1, "group": "stroke", "type": "Number" }; 6 | lineJoin = { "value": "miter", "default": "miter", "group": "stroke", "type": "String", enums: ["miter", "round", "bevel"] }; 7 | lineCap = { "value": "butt", "default": "butt", "group": "stroke", "type": "String", enums: ["butt", "round", "square"] }; 8 | dash = { "value": [], "default": [], "group": "stroke", "type": "Array", enums: [[2, 2], [5, 5], [10, 10]] }; 9 | shadowColor = { "value": "#ccc", "default": "#ccc", "group": "shadow", "type": "String" }; 10 | shadowBlur = { "value": 0, "default": 0, "group": "shadow", "type": "Number" }; 11 | shadowOffsetX = { "value": 0, "default": 0, "group": "shadow", "type": "Number" }; 12 | shadowOffsetY = { "value": 0, "default": 0, "group": "shadow", "type": "Number" }; 13 | shadowOpacity = { "value": 1, "default": 1, "group": "shadow", "type": "Number" }; 14 | hitStrokeWidth = { "value": "auto", "default": "auto", "group": "hidden", "type": "Number" }; 15 | strokeScaleEnabled = { "value": true, "default": true, "group": "hidden", "type": "Boolean" }; 16 | fill = { "value": "", "default": "", "group": "fill", "type": "String" }; 17 | fillGradient = { "value": "", "default": "", "group": "fill", "type": "String", enums: ['', 'linear', 'radial'] }; 18 | fillLinearGradientStartPointX = { "value": 0, "default": 0, "group": "fill", "type": "Number" }; 19 | fillLinearGradientStartPointY = { "value": 0, "default": 0, "group": "fill", "type": "Number" }; 20 | fillLinearGradientEndPointX = { "value": 0, "default": 0, "group": "fill", "type": "Number" }; 21 | fillLinearGradientEndPointY = { "value": 0, "default": 0, "group": "fill", "type": "Number" }; 22 | fillLinearGradientColorStops = { "value": null, "default": null, "group": "fill", "type": "Array", enums: [[0, '#bdc3c7', 1, '#2c3e50'], [0, '#ee9ca7', 1, '#ffdde1'], [0, '#2193b0', 1, '#6dd5ed']] }; 23 | fillRadialGradientStartRadius = { "value": 0, "default": 0, "group": "fill", "type": "Number" }; 24 | fillRadialGradientEndRadius = { "value": 0, "default": 0, "group": "fill", "type": "Number" }; 25 | fillRadialGradientColorStops = { "value": null, "default": null, "group": "fill", "type": "Array", enums: [[0, '#bdc3c7', 1, '#2c3e50'], [0, '#ee9ca7', 1, '#ffdde1'], [0, '#2193b0', 1, '#6dd5ed']] }; 26 | } 27 | 28 | export abstract class ShapeNode extends Node { 29 | 30 | className = 'ShapeNode'; 31 | 32 | updateRefAttrs(attrValues: any) { 33 | if (this.ref !== null) { 34 | if (attrValues.hasOwnProperty('fillGradient')) { 35 | let isRadial = attrValues['fillGradient'] === 'radial'; 36 | let isLinear = attrValues['fillGradient'] === 'linear'; 37 | let radialAttrNames = ['fillRadialGradientStartRadius', 'fillRadialGradientEndRadius', 'fillRadialGradientColorStops']; 38 | let linearAttrNames = ['fillLinearGradientStartPointX', 'fillLinearGradientStartPointY', 'fillLinearGradientEndPointX', 'fillLinearGradientEndPointY', 'fillLinearGradientColorStops']; 39 | let obj: any = {}, attributes = this.attributes; 40 | for (let key of radialAttrNames) { 41 | obj[key] = isRadial ? attributes[key].value : attributes[key].default; 42 | } 43 | for (let key of linearAttrNames) { 44 | obj[key] = isLinear ? attributes[key].value : attributes[key].default; 45 | } 46 | Object.assign(attrValues, obj); 47 | } 48 | super.updateRefAttrs(attrValues); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/model/StarNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class StarNodeAttrs extends ShapeNodeAttrs { 6 | numPoints = { "value": 0, "default": 0, "group": "geometry", "type": "Int" }; 7 | innerRadius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 8 | outerRadius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 9 | } 10 | 11 | export class StarNode extends ShapeNode { 12 | 13 | static className = 'StarNode'; 14 | className = 'StarNode'; 15 | attributes = new StarNodeAttrs(); 16 | 17 | constructor(opt?: any) { 18 | super(); 19 | if (opt) this.fromObject(opt); 20 | } 21 | 22 | createRef() { 23 | let konvaNode = new Konva.Star(); 24 | konvaNode.id(this.id); 25 | this.ref = konvaNode; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/model/StraightConnectedLineNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { EditableShapeNode, EditableShapeNodeAttrs } from './EditableShapeNode'; 4 | import Command from '../command/Command'; 5 | import AttributeChange from '../command/AttributeChange'; 6 | import { Utils } from '../Utils'; 7 | import { BaseConnectedLineNode, BaseConnectedLineNodeAttrs } from './BaseConnectedLineNode'; 8 | 9 | export class StraightConnectedLineNodeAttrs extends BaseConnectedLineNodeAttrs { 10 | 11 | } 12 | 13 | export class StraightConnectedLineNode extends BaseConnectedLineNode { 14 | 15 | static className = 'StraightConnectedLineNode'; 16 | className = 'StraightConnectedLineNode'; 17 | attributes: any = new StraightConnectedLineNodeAttrs(); 18 | 19 | constructor(opt?: any) { 20 | super(); 21 | if (opt) this.fromObject(opt); 22 | } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/model/SymbolNode.ts: -------------------------------------------------------------------------------- 1 | import { ContainerNode, ContainerNodeAttrs } from './ContainerNode'; 2 | 3 | export class SymbolNodeAttrs extends ContainerNodeAttrs { } 4 | 5 | export class SymbolNode extends ContainerNode { 6 | 7 | static className = 'SymbolNode'; 8 | className = 'SymbolNode'; 9 | attributes = new SymbolNodeAttrs(); 10 | symbolName: string | undefined; 11 | 12 | constructor(opt?: any) { 13 | super(); 14 | if (opt) this.fromObject(opt); 15 | } 16 | 17 | getSymbolName() { 18 | return this.symbolName; 19 | } 20 | 21 | setSymbolName(name: string) { 22 | this.symbolName = name; 23 | } 24 | 25 | fromObject(obj: any) { 26 | super.fromObject(obj); 27 | let symbolName: string; 28 | if (obj instanceof Array) { 29 | symbolName = obj[8]; 30 | } else { 31 | ({ symbolName } = obj); 32 | } 33 | if (symbolName) this.setSymbolName(symbolName); 34 | } 35 | 36 | toObject(isArray: boolean = false, distinct: boolean = true,unique:boolean=false) { 37 | let obj: any = super.toObject(isArray,distinct,unique); 38 | let symbolName = this.getSymbolName(); 39 | if (isArray) { 40 | obj.push(symbolName); 41 | } else { 42 | obj.symbolName = symbolName; 43 | } 44 | return obj; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/model/TextNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | import Command from "../command/Command"; 5 | import AttributeChange from '../command/AttributeChange'; 6 | 7 | export class TextNodeAttrs extends ShapeNodeAttrs { 8 | align = { "value": "left", "default": "left", "group": "geometry", "type": "String", enums: ["left", "center", "right"] }; 9 | verticalAlign = { "value": "top", "default": "top", "group": "geometry", "type": "String", enums: ["top", "middle", "bottom"] }; 10 | padding = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 11 | lineHeight = { "value": 1, "default": 1, "group": "geometry", "type": "Number" }; 12 | wrap = { "value": "word", "default": "word", "group": "geometry", "type": "String", enums: ["word", "char", "none"] }; 13 | ellipsis = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 14 | fontFamily = { "value": "Arial", "default": "Arial", "group": "font", "type": "String", enums: ["宋体", "Arial", "Calibri"] }; 15 | fontSize = { "value": 12, "default": 12, "group": "font", "type": "Number" }; 16 | fontStyle = { "value": "normal", "default": "normal", "group": "font", "type": "String", enums: ["normal", "bold", "italic", "bold italic"] }; 17 | fontVariant = { "value": "normal", "default": "normal", "group": "font", "type": "String", enums: ["normal", "small-caps"] }; 18 | textDecoration = { "value": "", "default": "", "group": "font", "type": "String", enums: ["", "underline", "line-through", "underline line-through"] }; 19 | text = { "value": "", "default": "", "group": "text", "type": "String" }; 20 | } 21 | 22 | export class TextNode extends ShapeNode { 23 | 24 | static className = 'TextNode'; 25 | className = 'TextNode'; 26 | attributes = new TextNodeAttrs(); 27 | 28 | constructor(opt?: any) { 29 | super(); 30 | if (opt) this.fromObject(opt); 31 | } 32 | 33 | createRef() { 34 | let konvaNode = new Konva.Text(); 35 | konvaNode.id(this.id); 36 | this.ref = konvaNode; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/model/WedgeNode.ts: -------------------------------------------------------------------------------- 1 | 2 | import Konva from 'konva'; 3 | import { ShapeNode, ShapeNodeAttrs } from './ShapeNode'; 4 | 5 | export class WedgeNodeAttrs extends ShapeNodeAttrs { 6 | angle = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 7 | radius = { "value": 0, "default": 0, "group": "geometry", "type": "Number" }; 8 | clockwise = { "value": false, "default": false, "group": "geometry", "type": "Boolean" }; 9 | } 10 | 11 | export class WedgeNode extends ShapeNode { 12 | 13 | static className = 'WedgeNode'; 14 | className:string='WedgeNode'; 15 | attributes = new WedgeNodeAttrs(); 16 | 17 | constructor(opt?: any) { 18 | super(); 19 | if (opt) this.fromObject(opt); 20 | } 21 | 22 | createRef() { 23 | let konvaNode = new Konva.Wedge(); 24 | konvaNode.id(this.id); 25 | this.ref = konvaNode; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/shape/AbstractShape.ts: -------------------------------------------------------------------------------- 1 | class AbstractShape { 2 | 3 | handleTagName: string | undefined; 4 | 5 | constructor() { } 6 | 7 | insertShapeElement(dataModel: any, node: any) { 8 | dataModel.addNodes([node]); 9 | } 10 | 11 | } 12 | 13 | export default AbstractShape; -------------------------------------------------------------------------------- /src/shape/ArrowShape.ts: -------------------------------------------------------------------------------- 1 | 2 | import BaseLineShape from './BaseLineShape'; 3 | import { LineArrowNode } from './../model/LineArrowNode'; 4 | 5 | class ArrowShape extends BaseLineShape { 6 | 7 | constructor() { 8 | super(); 9 | this.handleTagName = 'lineArrow'; 10 | } 11 | 12 | createElement(firstPoint: any, point: any, graphEditor: any) { 13 | let lineArrowNode: any = new LineArrowNode(); 14 | let defaultStyleConfig = graphEditor.getConfig().style; 15 | let lineStyle = { 16 | 'x': 0, 17 | 'y': 0, 18 | 'x1': firstPoint.x, 19 | 'y1': firstPoint.y, 20 | 'x2': point.x, 21 | 'y2': point.y, 22 | 'fill': defaultStyleConfig.stroke 23 | }; 24 | let style = { ...lineStyle, ...defaultStyleConfig }; 25 | lineArrowNode.setAttributeValues(style); 26 | return lineArrowNode; 27 | } 28 | 29 | } 30 | 31 | export default ArrowShape; -------------------------------------------------------------------------------- /src/shape/BaseConnectedLineShape.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import { DRAWING_MOUSE_DOWN, DRAWING_MOUSE_MOVE, DRAWING_MOUSE_UP } from '../constants/TemcConstants'; 3 | import { LineNode } from '../model/LineNode'; 4 | import BaseLineShape from './BaseLineShape'; 5 | import AbstractShape from './AbstractShape'; 6 | 7 | class BaseConnectedLineShape extends AbstractShape { 8 | createElement(firstPoint: any, point: any, graphEditor: any) { 9 | throw new Error('Method not implemented.'); 10 | } 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | getOriginPoint(konvaNode:any,point:any,stageTf:Konva.Transform){ 17 | console.log(konvaNode.getAbsoluteTransform()); 18 | let tfWithoutStage=konvaNode.getAbsoluteTransform().copy().multiply(stageTf.invert()); 19 | 20 | console.log(tfWithoutStage); 21 | let nodeTf=tfWithoutStage.invert(); 22 | return nodeTf.point(point); 23 | 24 | } 25 | 26 | 27 | 28 | } 29 | 30 | export default BaseConnectedLineShape; 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/shape/BaseLineShape.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import AbstractShape from "./AbstractShape"; 3 | import { DRAWING_MOUSE_DOWN, DRAWING_MOUSE_MOVE, DRAWING_MOUSE_UP, REGULAR_MODE,DRAWING_MOUSE_OUT } from '../constants/TemcConstants'; 4 | 5 | abstract class BaseLineShape extends AbstractShape { 6 | 7 | firstPoint: any; 8 | tempLine: any; 9 | 10 | constructor() { 11 | super(); 12 | } 13 | 14 | notifyDrawingAction(graphEditor: any, point: any, type: number, btn: number) { 15 | let isSquare = graphEditor.getIsSquare(); 16 | switch (type) { 17 | case DRAWING_MOUSE_DOWN: 18 | if (btn === 0) { 19 | this.firstPoint = point; 20 | this.tempLine = new Konva.Line({ 21 | x: 0, 22 | y: 0, 23 | points: [this.firstPoint.x, this.firstPoint.y], 24 | strokeWidth: 2, 25 | stroke: '#ff0000' 26 | }) 27 | graphEditor.getDrawingLayer().add(this.tempLine); 28 | let line = new Konva.Line({ 29 | x: 100, 30 | y: 50, 31 | points: [73, 70], 32 | stroke: 'red', 33 | tension: 1 34 | }); 35 | graphEditor.getDrawingLayer().add(line); 36 | } else { 37 | if (this.firstPoint && this.tempLine) { 38 | if (isSquare) { 39 | point = this.relocationPoint(this.firstPoint, point); 40 | } 41 | let node = this.createElement(this.firstPoint, point, graphEditor); 42 | this.tempLine.destroy(); 43 | if (node) { 44 | this.insertShapeElement(graphEditor.getDataModel(), node); 45 | } 46 | 47 | } 48 | if (this.tempLine) { 49 | this.tempLine.destroy(); 50 | } 51 | } 52 | 53 | break; 54 | case DRAWING_MOUSE_MOVE: 55 | if (this.firstPoint && this.tempLine) { 56 | if (isSquare) { 57 | point = this.relocationPoint(this.firstPoint, point); 58 | } 59 | this.tempLine.setAttrs({ 60 | points: [this.firstPoint.x, this.firstPoint.y, point.x, point.y], 61 | }) 62 | } 63 | break; 64 | case DRAWING_MOUSE_UP: 65 | if (this.firstPoint && this.tempLine) { 66 | if (isSquare) { 67 | point = this.relocationPoint(this.firstPoint, point); 68 | } 69 | let node = this.createElement(this.firstPoint, point, graphEditor); 70 | this.tempLine.destroy(); 71 | if (node) { 72 | this.insertShapeElement(graphEditor.getDataModel(), node); 73 | } 74 | 75 | } 76 | if (this.tempLine) { 77 | this.tempLine.destroy(); 78 | } 79 | break; 80 | case DRAWING_MOUSE_OUT: 81 | //销毁对象 82 | if (this.tempLine) { 83 | this.tempLine.destroy(); 84 | this.tempLine = null; 85 | } 86 | 87 | break; 88 | } 89 | 90 | } 91 | 92 | abstract createElement(firstPoint: any, point: any, graphEditor: any): any; 93 | 94 | relocationPoint(firstPoint: any, currentPoint: any) { 95 | let returnPoint = currentPoint; 96 | //获取斜率 97 | let k = (currentPoint.y - firstPoint.y) / (currentPoint.x - firstPoint.x); 98 | // 根据斜率来判断是什么样的直线 99 | if (k > -0.5 && k < 0.5) { 100 | //水平的直线 101 | returnPoint.x = currentPoint.x; 102 | returnPoint.y = firstPoint.y; 103 | } else if (k > 10 || k < -10) { 104 | //垂直的直线 105 | returnPoint.x = firstPoint.x; 106 | returnPoint.y = currentPoint.y; 107 | } else { 108 | //生成和坐标轴为45度角的直线 109 | if (k > 0) { 110 | returnPoint.x = currentPoint.x; 111 | returnPoint.y = currentPoint.x - firstPoint.x + firstPoint.y; 112 | } else { 113 | returnPoint.x = currentPoint.x; 114 | returnPoint.y = firstPoint.x + firstPoint.y - currentPoint.x; 115 | } 116 | } 117 | return returnPoint; 118 | } 119 | 120 | } 121 | 122 | export default BaseLineShape; -------------------------------------------------------------------------------- /src/shape/BasePolylineShape.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import AbstractShape from "./AbstractShape"; 3 | import AttributeChange from '../command/AttributeChange'; 4 | import Command from "../command/Command"; 5 | import { DRAWING_MOUSE_DBL_CLICK, DRAWING_MOUSE_CLICK, DRAWING_MOUSE_MOVE, DRAWING_MOUSE_UP, REGULAR_MODE,DRAWING_MOUSE_OUT } from '../constants/TemcConstants'; 6 | import { PolylineNode } from '../model/PolylineNode'; 7 | 8 | class BasePolylineShape extends AbstractShape { 9 | 10 | tempLine: any; 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | createElement(points: any, graphEditor: any) { 17 | let polyline = new PolylineNode(); 18 | let style = Object.assign({ points }, graphEditor.getConfig().style); 19 | polyline.setAttributeValues({ ...style, hitStrokeWidth: style.strokeWidth * 2 + 8 }); 20 | return polyline; 21 | } 22 | 23 | notifyDrawingAction(graphEditor: any, point: any, type: number, btn: number) { 24 | let isSquare = graphEditor.getIsSquare(); 25 | switch (type) { 26 | case DRAWING_MOUSE_CLICK: 27 | 28 | if (btn === 0) { 29 | if (this.tempLine) { 30 | let points = this.tempLine.points(); 31 | points.push(point.x, point.y); 32 | this.tempLine.points(points); 33 | } else { 34 | this.tempLine = new Konva.Line({ 35 | points: [point.x, point.y, point.x, point.y], 36 | strokeWidth: 2, 37 | stroke: '#ff0000' 38 | }); 39 | graphEditor.getDrawingLayer().add(this.tempLine); 40 | } 41 | }else{ 42 | 43 | if (this.tempLine) { 44 | 45 | let points = this.tempLine.points(); 46 | if(points.length==2) return 47 | let newPoints = JSON.parse(JSON.stringify(points)); 48 | let polyline = this.createElement(newPoints, graphEditor); 49 | this.insertShapeElement(graphEditor.getDataModel(), polyline); 50 | this.tempLine.destroy(); 51 | this.tempLine = null; 52 | } 53 | } 54 | break; 55 | case DRAWING_MOUSE_DBL_CLICK: 56 | if (this.tempLine) { 57 | 58 | let points = this.tempLine.points(); 59 | points.pop(); 60 | points.pop(); 61 | points.pop(); 62 | points.pop(); 63 | if(points.length==2) return 64 | let newPoints = JSON.parse(JSON.stringify(points)); 65 | let polyline = this.createElement(newPoints, graphEditor); 66 | this.insertShapeElement(graphEditor.getDataModel(), polyline); 67 | this.tempLine.destroy(); 68 | this.tempLine = null; 69 | } 70 | 71 | break; 72 | case DRAWING_MOUSE_MOVE: 73 | if (this.tempLine) { 74 | let points = this.tempLine.points(); 75 | points.pop(); 76 | points.pop(); 77 | let len=points.length; 78 | let previousPoint={x:points[len-2],y:points[len-1]}; 79 | if(isSquare){ 80 | point = this.reLocateLinePointWhenDrawing(previousPoint, point); 81 | } 82 | points.push(point.x, point.y); 83 | this.tempLine.points(points); 84 | } 85 | break; 86 | case DRAWING_MOUSE_OUT: 87 | //销毁对象 88 | if(this.tempLine){ 89 | this.tempLine.destroy(); 90 | this.tempLine = null; 91 | } 92 | 93 | break; 94 | case DRAWING_MOUSE_UP: 95 | 96 | break; 97 | } 98 | } 99 | reLocateLinePointWhenDrawing(oldPoint:any,point:any){ 100 | 101 | //获取斜率 102 | let k = (point.y - oldPoint.y) 103 | / (point.x - oldPoint.x); 104 | 105 | // 根据斜率来判断是什么样的直线 106 | if (k > -0.5 && k < 0.5) { 107 | //水平的直线 108 | return {x:point.x,y:oldPoint.y}; 109 | } else if (k > 10 || k < -10) { 110 | //垂直的直线 111 | return {x:oldPoint.x, y:point.y}; 112 | } else { 113 | //生成和坐标轴为45度角的直线 114 | if (k > 0) { 115 | return {x:point.x, y:point.x 116 | - oldPoint.x 117 | + oldPoint.y}; 118 | } else { 119 | return {x:point.x, y:oldPoint.x 120 | + oldPoint.y - point.x}; 121 | } 122 | } 123 | 124 | } 125 | } 126 | 127 | export default BasePolylineShape; -------------------------------------------------------------------------------- /src/shape/LineShape.ts: -------------------------------------------------------------------------------- 1 | import { LineNode } from './../model/LineNode'; 2 | import BaseLineShape from './BaseLineShape'; 3 | 4 | class LineShape extends BaseLineShape { 5 | 6 | constructor() { 7 | super(); 8 | this.handleTagName = 'line'; 9 | } 10 | 11 | createElement(firstPoint: any, point: any, graphEditor: any) { 12 | if (firstPoint.x == point.x && firstPoint.y == point.y) { 13 | return 14 | } 15 | let lineNode: any = new LineNode(); 16 | let defaultStyleConfig = graphEditor.getConfig().style; 17 | 18 | let lineStyle = { 19 | 'x': 0, 20 | 'y': 0, 21 | 'x1': firstPoint.x, 22 | 'y1': firstPoint.y, 23 | 'x2': point.x, 24 | 'y2': point.y 25 | }; 26 | let style = { ...lineStyle, ...defaultStyleConfig }; 27 | lineNode.setAttributeValues(style); 28 | return lineNode; 29 | 30 | 31 | } 32 | 33 | } 34 | 35 | export default LineShape; -------------------------------------------------------------------------------- /src/shape/PenShape.ts: -------------------------------------------------------------------------------- 1 | import Konva from "konva"; 2 | import { DRAWING_MOUSE_DOWN, DRAWING_MOUSE_MOVE, DRAWING_MOUSE_OUT, DRAWING_MOUSE_UP, REGULAR_MODE } from "../constants/TemcConstants"; 3 | import { PenNode } from "../model/PenNode"; 4 | import AbstractShape from "./AbstractShape"; 5 | 6 | class PenShape extends AbstractShape { 7 | 8 | firstPoint: any; 9 | tempLine: any; 10 | 11 | constructor() { 12 | super(); 13 | this.handleTagName = 'pen'; 14 | } 15 | 16 | notifyDrawingAction(graphEditor: any, point: any, type: number, btn: number) { 17 | switch (type) { 18 | case DRAWING_MOUSE_DOWN: 19 | if (btn === 0) { 20 | this.firstPoint = point; 21 | this.tempLine = new Konva.Line({ 22 | x: 0, 23 | y: 0, 24 | points: [this.firstPoint.x, this.firstPoint.y], 25 | strokeWidth: 2, 26 | stroke: '#ff0000' 27 | }) 28 | graphEditor.getHelpLayer().add(this.tempLine); 29 | } else { 30 | if (this.firstPoint) { 31 | let node = this.createElement(this.tempLine.points(), graphEditor); 32 | this.tempLine.destroy(); 33 | if (node) { 34 | this.insertShapeElement(graphEditor.getDataModel(), node); 35 | } 36 | 37 | } 38 | if (this.tempLine) { 39 | this.tempLine.destroy(); 40 | this.tempLine = null; 41 | } 42 | this.firstPoint = null; 43 | } 44 | 45 | break; 46 | case DRAWING_MOUSE_MOVE: 47 | if (this.firstPoint && this.tempLine) { 48 | let points = this.tempLine.points(); 49 | points.push(point.x); 50 | points.push(point.y); 51 | this.tempLine.setAttrs({ 52 | points: points, 53 | }) 54 | } 55 | break; 56 | case DRAWING_MOUSE_UP: 57 | 58 | if (this.firstPoint && this.tempLine) { 59 | let node = this.createElement(this.tempLine.points(), graphEditor); 60 | this.tempLine.destroy(); 61 | if (node) { 62 | this.insertShapeElement(graphEditor.getDataModel(), node); 63 | } 64 | } 65 | if (this.tempLine) { 66 | this.tempLine.destroy(); 67 | this.tempLine = null; 68 | } 69 | this.firstPoint = null; 70 | break; 71 | case DRAWING_MOUSE_OUT: 72 | //销毁对象 73 | if(this.tempLine){ 74 | this.tempLine.destroy(); 75 | this.tempLine = null; 76 | } 77 | 78 | break; 79 | } 80 | } 81 | 82 | createElement(points: any, graphEditor: any) { 83 | if (points.length == 2) 84 | return 85 | let polyline = new PenNode(); 86 | let style = Object.assign({ points }, graphEditor.getConfig().style); 87 | polyline.setAttributeValues({ ...style }); 88 | return polyline; 89 | } 90 | 91 | } 92 | 93 | export default PenShape; -------------------------------------------------------------------------------- /src/shape/PolylineArrowShape.ts: -------------------------------------------------------------------------------- 1 | 2 | import BasePolylineShape from './BasePolylineShape'; 3 | import { PolylineArrowNode } from './../model/PolylineArrowNode'; 4 | 5 | class PolylineArrowShape extends BasePolylineShape { 6 | 7 | constructor() { 8 | super(); 9 | this.handleTagName = 'polylineArrow'; 10 | } 11 | 12 | createElement(points: any, graphEditor: any) { 13 | let polyline = new PolylineArrowNode(); 14 | let style = Object.assign({ points, 'fill': graphEditor.getConfig().style.stroke }, graphEditor.getConfig().style); 15 | polyline.setAttributeValues({ ...style, hitStrokeWidth: style.strokeWidth * 2 + 8 }); 16 | return polyline; 17 | } 18 | 19 | } 20 | 21 | export default PolylineArrowShape; -------------------------------------------------------------------------------- /src/shape/PolylineShape.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import AbstractShape from "./AbstractShape"; 3 | import { DRAWING_MOUSE_DOWN, DRAWING_MOUSE_MOVE, DRAWING_MOUSE_UP, REGULAR_MODE } from './../constants/TemcConstants'; 4 | import { PolylineNode } from './../model/PolylineNode'; 5 | import BasePolylineShape from './BasePolylineShape'; 6 | 7 | class PolylineShape extends BasePolylineShape { 8 | 9 | constructor() { 10 | super(); 11 | this.handleTagName = 'polyline'; 12 | } 13 | 14 | createElement(points: any, graphEditor: any) { 15 | let polyline = new PolylineNode(); 16 | let style = Object.assign({ points }, graphEditor.getConfig().style); 17 | polyline.setAttributeValues({ ...style, hitStrokeWidth: style.strokeWidth * 2 + 8 }); 18 | return polyline; 19 | } 20 | } 21 | 22 | export default PolylineShape; -------------------------------------------------------------------------------- /src/shape/StraightConnectedLineShape.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | import { DRAWING_MOUSE_DOWN, DRAWING_MOUSE_MOVE, DRAWING_MOUSE_UP } from '../constants/TemcConstants'; 3 | import { LineNode } from '../model/LineNode'; 4 | import BaseLineShape from './BaseLineShape'; 5 | 6 | import AbstractShape from './AbstractShape'; 7 | import BaseConnectedLineShape from './BaseConnectedLineShape'; 8 | import { StraightConnectedLineNode } from '../model/StraightConnectedLineNode'; 9 | 10 | class StraightConnectedLineShape extends BaseConnectedLineShape { 11 | firstPoint: any; 12 | tempLine: any; 13 | createElement(firstPoint: any, point: any, graphEditor: any) { 14 | throw new Error('Method not implemented.'); 15 | } 16 | 17 | constructor() { 18 | super(); 19 | this.handleTagName = 'straightConnectedLine'; 20 | } 21 | notifyDrawingAction(graphEditor: any, point: any, type: number) { 22 | 23 | let isSquare = graphEditor.getIsSquare(); 24 | switch (type) { 25 | case DRAWING_MOUSE_DOWN: 26 | console.log('StraightConnectedLineShape notifyDrawingAction mousedonw') 27 | if(graphEditor.currentTarget!=null){ 28 | console.log(graphEditor.currentTarget) 29 | this.firstPoint = point; 30 | let fromPoint=this.getOriginPoint(graphEditor.currentTarget,point,graphEditor.stage.getAbsoluteTransform().copy()); 31 | let sourceId=graphEditor.currentTarget.attrs.id; 32 | console.log(sourceId); 33 | this.tempLine = new Konva.Line({ 34 | x: 0, 35 | y: 0, 36 | points: [this.firstPoint.x, this.firstPoint.y], 37 | strokeWidth: 2, 38 | stroke: '#ff0000', 39 | 40 | }) 41 | this.tempLine.from={ 42 | id:sourceId, 43 | point:fromPoint 44 | } 45 | graphEditor.getDrawingLayer().add(this.tempLine); 46 | let line = new Konva.Line({ 47 | x: 100, 48 | y: 50, 49 | points: [73, 70], 50 | stroke: 'red', 51 | tension: 1 52 | }); 53 | graphEditor.getDrawingLayer().add(line); 54 | } 55 | 56 | break; 57 | case DRAWING_MOUSE_MOVE: 58 | 59 | 60 | if (this.firstPoint && this.tempLine) { 61 | this.tempLine.setAttrs({ 62 | points: [this.firstPoint.x, this.firstPoint.y, point.x, point.y], 63 | }) 64 | } 65 | break; 66 | case DRAWING_MOUSE_UP: 67 | if (this.firstPoint && graphEditor.currentTarget) { 68 | let toPoint=this.getOriginPoint(graphEditor.currentTarget,point,graphEditor.stage.getAbsoluteTransform().copy()); 69 | let disTargetId=graphEditor.currentTarget.attrs.id; 70 | let node = this.createElementWithTarget(this.firstPoint, point, graphEditor,this.tempLine.from,{ 71 | id:disTargetId, 72 | point:toPoint 73 | }); 74 | this.tempLine.destroy(); 75 | if(node){ 76 | this.insertShapeElement(graphEditor.getDataModel(), node); 77 | } 78 | 79 | } 80 | if (this.tempLine) { 81 | this.tempLine.destroy(); 82 | } 83 | break; 84 | } 85 | 86 | } 87 | 88 | 89 | createElementWithTarget(firstPoint: any, point: any, graphEditor: any,fromInfo:any,toInfo:any) { 90 | if (firstPoint.x == point.x && firstPoint.y == point.y) { 91 | return 92 | } 93 | let lineNode: any = new StraightConnectedLineNode(); 94 | let defaultStyleConfig = graphEditor.getConfig().style; 95 | 96 | let lineStyle = { 97 | 'x': 0, 98 | 'y': 0, 99 | 'points': [firstPoint.x,firstPoint.y,point.x,point.y], 100 | 'draggable':false 101 | 102 | }; 103 | let style = {...defaultStyleConfig,...lineStyle}; 104 | console.log(style); 105 | lineNode.setFrom(fromInfo); 106 | lineNode.setTo(toInfo); 107 | lineNode.setAttributeValues(style); 108 | 109 | return lineNode; 110 | 111 | 112 | } 113 | 114 | 115 | } 116 | 117 | export default StraightConnectedLineShape; 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/snapGrid/SnapGrid.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva' 2 | 3 | 4 | import type { 5 | GuideLine, 6 | LineStops, 7 | NodeEdgeBound, 8 | Orientation, 9 | Nullable, 10 | EditorConfig 11 | } from '../types' 12 | import GraphEditor from '../GraphEditor' 13 | 14 | 15 | export class SnapGrid { 16 | 17 | private editor: GraphEditor 18 | 19 | 20 | private lines: Konva.Line[] = [] 21 | 22 | 23 | private GUIDELINE_OFFSET = 5 24 | 25 | 26 | private options?: Nullable = {} 27 | private active = false 28 | stage: any 29 | private lineGuideStops: LineStops; 30 | 31 | /** 32 | * 构造函数 33 | * @param editor GraphEditor对象 34 | */ 35 | constructor(editor: GraphEditor) { 36 | let config = editor.config as EditorConfig; 37 | const options = config.drawing?.snapToGrid 38 | 39 | this.editor = editor 40 | this.options = options 41 | if (options?.enable) { 42 | this.active = options.enable; 43 | } 44 | if (this.active && this.editor) { 45 | this.stage = this.editor.stage; 46 | this.registerEvents() 47 | } 48 | } 49 | 50 | 51 | /** 52 | * Creates list of line stops to find guide lines 53 | * 54 | * @param konvaNode the dragging node 55 | * @returns the list of line stops 56 | */ 57 | private getLineGuideStops(konvaNode: Konva.Shape): LineStops { 58 | var vertical = [0, this.stage.width() / 2, this.stage.width()]; 59 | var horizontal = [0, this.stage.height() / 2, this.stage.height()]; 60 | 61 | this.editor.getDataModel()?.getNodes().forEach((shape: any) => { 62 | 63 | if (shape.getRef() === konvaNode) { 64 | return 65 | } 66 | 67 | const box = shape.getRef().getClientRect() 68 | 69 | vertical.push([box.x, box.x + box.width, box.x + box.width / 2]); 70 | horizontal.push([box.y, box.y + box.height, box.y + box.height / 2]); 71 | }) 72 | 73 | return { 74 | vertical: vertical.flat(), 75 | horizontal: horizontal.flat(), 76 | }; 77 | } 78 | 79 | // what points of the object will trigger to snapping? 80 | // it can be just center of the object 81 | // but we will enable all edges and center 82 | private getObjectSnappingEdges(node: any) { 83 | var box = node.getClientRect(); 84 | var absPos = node.absolutePosition(); 85 | return { 86 | vertical: [ 87 | { 88 | guide: Math.round(box.x), 89 | offset: Math.round(absPos.x - box.x), 90 | snap: 'start', 91 | }, 92 | { 93 | guide: Math.round(box.x + box.width / 2), 94 | offset: Math.round(absPos.x - box.x - box.width / 2), 95 | snap: 'center', 96 | }, 97 | { 98 | guide: Math.round(box.x + box.width), 99 | offset: Math.round(absPos.x - box.x - box.width), 100 | snap: 'end', 101 | }, 102 | ], 103 | horizontal: [ 104 | { 105 | guide: Math.round(box.y), 106 | offset: Math.round(absPos.y - box.y), 107 | snap: 'start', 108 | }, 109 | { 110 | guide: Math.round(box.y + box.height / 2), 111 | offset: Math.round(absPos.y - box.y - box.height / 2), 112 | snap: 'center', 113 | }, 114 | { 115 | guide: Math.round(box.y + box.height), 116 | offset: Math.round(absPos.y - box.y - box.height), 117 | snap: 'end', 118 | }, 119 | ], 120 | }; 121 | } 122 | 123 | /** 124 | * Creates the guide lines based on the given line stops and edge bounds 125 | * 126 | * @param lineStops the line stops 127 | * @param nodeEdgeBounds the node edge bounds 128 | * @returns the list of guide lines 129 | */ 130 | private getGuides(lineGuideStops: any, itemBounds: any): GuideLine[] { 131 | var resultV: any = []; 132 | var resultH: any = []; 133 | 134 | lineGuideStops.vertical.forEach((lineGuide: number) => { 135 | itemBounds.vertical.forEach((itemBound: any) => { 136 | var diff = Math.abs(lineGuide - itemBound.guide); 137 | // if the distance between guild line and object snap point is close we can consider this for snapping 138 | if (diff < this.GUIDELINE_OFFSET) { 139 | resultV.push({ 140 | lineGuide: lineGuide, 141 | diff: diff, 142 | snap: itemBound.snap, 143 | offset: itemBound.offset, 144 | }); 145 | } 146 | }); 147 | }); 148 | 149 | lineGuideStops.horizontal.forEach((lineGuide: number) => { 150 | itemBounds.horizontal.forEach((itemBound: any) => { 151 | var diff = Math.abs(lineGuide - itemBound.guide); 152 | if (diff < this.GUIDELINE_OFFSET) { 153 | resultH.push({ 154 | lineGuide: lineGuide, 155 | diff: diff, 156 | snap: itemBound.snap, 157 | offset: itemBound.offset, 158 | }); 159 | } 160 | }); 161 | }); 162 | 163 | var guides: GuideLine[] = []; 164 | 165 | // find closest snap 166 | var minV = resultV.sort((a: any, b: any) => a.diff - b.diff)[0]; 167 | var minH = resultH.sort((a: any, b: any) => a.diff - b.diff)[0]; 168 | if (minV) { 169 | guides.push({ 170 | stop: minV.lineGuide, 171 | offset: minV.offset, 172 | orientation: 'vertical', 173 | snap: minV.snap, 174 | }); 175 | } 176 | if (minH) { 177 | guides.push({ 178 | stop: minH.lineGuide, 179 | offset: minH.offset, 180 | orientation: 'horizontal', 181 | snap: minH.snap, 182 | }); 183 | } 184 | return guides; 185 | } 186 | 187 | /** 188 | * Draws the guide lines 189 | * 190 | * @param guideLines the list of guide lines 191 | */ 192 | private drawGuides(guideLines: GuideLine[]) { 193 | if (!guideLines.length) { 194 | return 195 | } 196 | 197 | guideLines.forEach(guideLine => { 198 | const options: Konva.LineConfig = { 199 | stroke: '#000', 200 | strokeWidth: 1, 201 | dash: [2, 6], 202 | ...this.options 203 | } 204 | 205 | if (guideLine.orientation === 'vertical') { 206 | options.points = [0, 0, 0, this.stage.height()] 207 | } else if (guideLine.orientation === 'horizontal') { 208 | options.points = [0, 0, this.stage.width(), 0] 209 | } 210 | 211 | const line = new Konva.Line(options) 212 | //考虑line受stage的transform的影响 213 | const stageTransform = this.stage?.getAbsoluteTransform().copy(); 214 | stageTransform?.invert(); 215 | if (guideLine.orientation === 'vertical') { 216 | 217 | let translateFactor = stageTransform?.point({ 218 | x: guideLine.stop, 219 | y: 0 220 | }); 221 | line.absolutePosition(translateFactor); 222 | 223 | } else if (guideLine.orientation === 'horizontal') { 224 | let translateFactor = stageTransform?.point({ 225 | x: 0, 226 | y: guideLine.stop 227 | }); 228 | line.absolutePosition(translateFactor) 229 | } 230 | 231 | this.editor.getHelpLayer().add(line) 232 | this.lines.push(line) 233 | }) 234 | } 235 | 236 | /** 237 | * Sets the node's absolute position to the nearest snap line 238 | * 239 | * @param node The node of dragging shape 240 | * @param guideLines The list of guide lines 241 | */ 242 | private setNodePosition(node: Konva.Shape, guideLines: GuideLine[]) { 243 | guideLines.forEach(({ orientation, stop: stop, offset }) => { 244 | const position = node.absolutePosition() 245 | 246 | if (orientation === 'vertical') { 247 | position.x = stop + offset 248 | } else if (orientation === 'horizontal') { 249 | position.y = stop + offset 250 | } 251 | 252 | node.absolutePosition(position) 253 | }) 254 | } 255 | 256 | 257 | /** 258 | * 选中框的移动事件 259 | * @param event 事件 260 | */ 261 | private onDragMove(event: Konva.KonvaEventObject) { 262 | const transformer = event.target as unknown as Konva.Transformer; 263 | const node = transformer.nodes().length == 1 ? (transformer.nodes()[0] as Konva.Shape) : event.target as Konva.Shape 264 | this.destroy() 265 | // find possible snapping lines 266 | if (!this.lineGuideStops) { 267 | this.lineGuideStops = this.getLineGuideStops(node); 268 | } 269 | 270 | // find snapping points of current object 271 | var itemBounds = this.getObjectSnappingEdges(node); 272 | // now find where can we snap current object 273 | var guides = this.getGuides(this.lineGuideStops, itemBounds); 274 | // do nothing of no snapping 275 | if (!guides.length) { 276 | return; 277 | } 278 | this.drawGuides(guides); 279 | this.setNodePosition(node, guides) 280 | } 281 | 282 | /** 283 | * 移动结束后 284 | */ 285 | private onDragEnd() { 286 | this.lineGuideStops =null; 287 | this.destroy() 288 | } 289 | 290 | /** 291 | * 销毁 292 | */ 293 | private destroy() { 294 | if (this.lines.length > 0) { 295 | this.lines.forEach(line => line.destroy()) 296 | this.lines = [] 297 | } 298 | } 299 | 300 | /** 301 | * 注册事件 302 | */ 303 | private registerEvents() { 304 | this.editor.transformer.on('dragmove', this.onDragMove.bind(this)) 305 | this.editor.transformer.on('dragend', this.onDragEnd.bind(this)) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | import { NodeAttrs } from "../model/Node" 2 | 3 | 4 | export type Nullable = T | null 5 | 6 | export interface UnknownObject { 7 | [key: string]: boolean | number | string | object | null | undefined 8 | } 9 | export type AlignDirection = 'left'| 'right'| 'top' | 'bottom' | 'horizontal' | 'vertical' 10 | export type MoveDirection = 'left'| 'right'| 'up' | 'down' 11 | export type OrderDirection = 'top'| 'bottom'| 'up' | 'down' 12 | export const orderDirection = ["top", "bottom", "up", "down"]; 13 | export type EventType = 'click' | 'valuechange'| 'mousemove' | 'mouseout' | 'dblclick' 14 | export type EventAction = 'changeAttributes' | 'updateAnimation' | 'executeScript' 15 | export type EventWhenType = 'script'|'operation' 16 | export declare interface Dimensions { 17 | width: number 18 | height: number 19 | } 20 | 21 | export declare interface Point { 22 | x: number 23 | y: number 24 | } 25 | 26 | export declare interface NodeConfig { 27 | className: string 28 | attributes: any 29 | animation?:any 30 | variables?:any 31 | } 32 | 33 | export declare interface GEvent { 34 | type?:EventType, 35 | action?:EventAction, 36 | attributes?:any, 37 | animation?:boolean, 38 | script?:string, 39 | triggers?:[{ 40 | type?:EventWhenType, 41 | operation?:{ 42 | source?: any, 43 | operator?: string, 44 | target?: any, 45 | } 46 | script?:string 47 | }] 48 | } 49 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import type { Nullable } from '.' 5 | import type Konva from 'konva' 6 | 7 | export declare interface BaseConfig { 8 | container?: HTMLDivElement 9 | graph?:string 10 | style?:StyleConfig 11 | } 12 | export declare interface EditorConfig extends BaseConfig { 13 | 14 | view?: Partial<{ 15 | grid:GridConfig, 16 | size: Partial<{ 17 | width: number, 18 | height: number 19 | }> 20 | }> 21 | drawing?: Partial<{ 22 | snapToGrid: Partial<{ 23 | enable: boolean, 24 | strokeWidth: number, 25 | stroke: string, 26 | dash: Array 27 | }> 28 | editAnchor:Partial<{ 29 | anchorFill: string, 30 | anchorStroke: string 31 | }> 32 | connectable:boolean 33 | }> 34 | selection?: Partial<{ 35 | interactive: boolean 36 | keyboard: Partial< 37 | Nullable<{ 38 | enabled: boolean 39 | movingSpaces: number 40 | map: Partial< 41 | Nullable<{ 42 | delete?: string[] 43 | moveLeft?: string[] 44 | moveRight?: string[] 45 | moveUp?: string[] 46 | moveDown?: string[] 47 | moveLeftSlow?: string[] 48 | moveRightSlow?: string[] 49 | moveUpSlow?: string[] 50 | moveDownSlow?: string[] 51 | selectAll?: string[] 52 | deselect?: string[] 53 | inverseSelect?: string[] 54 | copy?: string[] 55 | paste?: string[] 56 | group?: string[] 57 | undo?:string[] 58 | redo?:string[] 59 | toSelectMode?:string[] 60 | }> 61 | > 62 | }> 63 | > 64 | transformer: Konva.TransformerConfig 65 | zone: Partial<{ 66 | fill: string, 67 | stroke: string 68 | }> 69 | }> 70 | history?: Partial<{ 71 | keyboard: { 72 | enabled: boolean 73 | } 74 | }> 75 | } 76 | 77 | export declare interface ViewerConfig extends BaseConfig{ 78 | 79 | } 80 | 81 | export declare interface StyleConfig{ 82 | strokeWidth?: number, 83 | stroke?: string, 84 | draggable?: boolean, 85 | strokeScaleEnabled?: boolean, 86 | hitStrokeWidth?: number, 87 | fill?:string 88 | } 89 | 90 | export declare interface GridConfig{ 91 | show?: boolean, 92 | distance?: number, 93 | color?: string 94 | } 95 | 96 | export declare type Config = Partial 97 | 98 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './snap-grid' 3 | export * from './config' -------------------------------------------------------------------------------- /src/types/snap-grid.ts: -------------------------------------------------------------------------------- 1 | export type Orientation = 'vertical' | 'horizontal' 2 | export type Snap = 'start' | 'end' | 'center' 3 | 4 | export type LineStops = Record | null 5 | 6 | export interface NodeEdgeBound { 7 | guide: number 8 | offset: number 9 | snap: Snap 10 | orientation: Orientation 11 | } 12 | 13 | export interface GuideLine { 14 | stop: number 15 | snap: Snap 16 | offset: number 17 | orientation: Orientation 18 | } 19 | -------------------------------------------------------------------------------- /test/performance-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KonvaJS Sandbox 7 | 9 | 16 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 开启动画 28 |
29 |
30 |
31 |
32 |
33 | 34 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /test/shape/basic.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const list = [ 4 | { 5 | label: '矩形', 6 | className: 'RectNode', 7 | attributes: { 8 | width: 100, 9 | height: 50, 10 | fill:'' 11 | }, 12 | path: '' 13 | }, 14 | { 15 | label: '椭圆', 16 | className: 'EllipseNode', 17 | attributes: { 18 | radiusX: 50, 19 | radiusY: 25 20 | }, 21 | path: '' 22 | }, 23 | { 24 | label: '正多边形', 25 | className: 'RegularPolygonNode', 26 | attributes: { 27 | sides: 6, 28 | radius: 40, 29 | }, 30 | path: '' 31 | }, 32 | { 33 | label: '星形', 34 | className: 'StarNode', 35 | attributes: { 36 | numPoints: 6, 37 | innerRadius: 20, 38 | outerRadius: 40, 39 | }, 40 | path: '' 41 | }, 42 | { 43 | label: '圆形', 44 | className: 'CircleNode', 45 | attributes: { 46 | radius: 50 47 | }, 48 | path: '' 49 | }, 50 | { 51 | label: '环形', 52 | className: 'RingNode', 53 | attributes: { 54 | innerRadius: 20, 55 | outerRadius: 40 56 | }, 57 | path: '' 58 | }, 59 | { 60 | label: '扇形', 61 | className: 'WedgeNode', 62 | attributes: { 63 | angle: 90, 64 | radius: 50, 65 | clockwise: true 66 | }, 67 | path: '' 68 | }, 69 | { 70 | label: '弧形', 71 | className: 'ArcNode', 72 | attributes: { 73 | angle: 30, 74 | innerRadius: 50, 75 | outerRadius: 100, 76 | clockwise: false 77 | }, 78 | path: '' 79 | }, 80 | { 81 | label: '心形', 82 | className: 'PathNode', 83 | attributes: { 84 | data: 'M213.1,6.7c-32.4-14.4-73.7,0-88.1,30.6C110.6,4.9,67.5-9.5,36.9,6.7C2.8,22.9-13.4,62.4,13.5,110.9C33.3,145.1,67.5,170.3,125,217c59.3-46.7,93.5-71.9,111.5-106.1C263.4,64.2,247.2,22.9,213.1,6.7z' 85 | }, 86 | path: '' 87 | }, 88 | { 89 | label:'风扇', 90 | className:'PathNode', 91 | attributes:{ 92 | scaleX:0.1, 93 | scaleY:0.1, 94 | data:'M696.8 504.5c0-11.7-1.4-23-3.4-34.1 2 11 3.4 22.4 3.4 34.1h260.8c0-135.6-60.6-256.8-155.8-338.8L632.4 364.6c-32.7-28.6-75-46.4-121.9-46.4-11.5 0-22.7 1.4-33.6 3.4 10.9-2 22.1-3.4 33.6-3.4V57.3c-137.1 0-259.6 61.8-341.6 158.9L368 385.8c-26.9 32.3-43.8 73.3-43.8 118.7 0 11.5 1.4 22.7 3.4 33.6-2-10.9-3.4-22.1-3.4-33.6H63.3c0 137.1 61.8 259.6 158.9 341.6L391.8 647c32.3 26.9 73.3 43.8 118.7 43.8 11.7 0 23-1.4 34.1-3.4-11.1 2.1-22.4 3.4-34.1 3.4v260.8c135.6 0 256.8-60.6 338.8-155.8l-199-169.4c28.6-32.8 46.5-75.1 46.5-121.9zM684 437.9c3.5 8.8 6.4 17.9 8.5 27.3-2.1-9.4-5.1-18.5-8.5-27.3z m-26-46.1c0.4 0.5 0.7 0.9 1.1 1.3-0.4-0.4-0.8-0.8-1.1-1.3z m10.9 15.7c4 6.4 7.7 13.1 10.9 20.1-3.2-7-7-13.6-10.9-20.1z m-198.1-84.9c-9.3 2-18.3 5-27 8.4 8.7-3.4 17.7-6.3 27-8.4z m-38.2 13.1c-6.9 3.2-13.5 6.8-19.9 10.8 6.4-4 13.1-7.6 19.9-10.8z m-35.2 21.7c-0.9 0.7-1.7 1.5-2.6 2.2 0.8-0.8 1.7-1.5 2.6-2.2zM337 571.2c-3.4-8.7-6.3-17.7-8.4-27 2 9.3 5 18.2 8.4 27z m28.5 49c-0.7-0.9-1.5-1.7-2.2-2.6 0.8 0.9 1.5 1.7 2.2 2.6z m-13-18z m46.2-97.7c0-61.6 50.1-111.8 111.8-111.8s111.8 50.1 111.8 111.8c0 61.6-50.1 111.8-111.8 111.8s-111.8-50.2-111.8-111.8zM623.1 652c-0.5 0.4-0.9 0.7-1.3 1.1 0.4-0.4 0.8-0.8 1.3-1.1z m-15.7 10.9c-6.4 4-13.1 7.7-20.1 10.9 7-3.2 13.7-7 20.1-10.9zM577 678c-8.8 3.5-17.9 6.4-27.3 8.5 9.5-2.1 18.5-5.1 27.3-8.5z' 95 | }, 96 | path:' ' 97 | 98 | 99 | }, 100 | { 101 | label: '文字', 102 | className: 'TextNode', 103 | attributes: { 104 | text: '文本信息', 105 | fontSize: 14, 106 | fontFamily: 'Calibri', 107 | padding:5, 108 | fill:'#ff0000', 109 | strokeWidth:0 110 | }, 111 | path: '' 112 | }, 113 | { 114 | label: '标签', 115 | className: 'LabelNode', 116 | attributes: { 117 | text: 'Tooltip', 118 | fontSize: 30, 119 | fontFamily: 'Calibri', 120 | textFill:'#ffffff', 121 | fill: 'black', 122 | pointerWidth: 10, 123 | pointerHeight: 10, 124 | lineJoin: 'round', 125 | shadowColor: 'black', 126 | shadowBlur: 10, 127 | shadowOffsetX: 10, 128 | shadowOffsetY: 10, 129 | shadowOpacity: 0.5, 130 | padding:10, 131 | pointerDirection:'down' 132 | }, 133 | 134 | path: '' 135 | }, 136 | { 137 | label: '图片', 138 | className: 'ImageNode', 139 | attributes: { 140 | width:200, 141 | height:200, 142 | fill:"", 143 | stroke:"", 144 | image:'https://i.postimg.cc/BQDBBTp3/image.jpg' 145 | }, 146 | url: 'https://i.postimg.cc/BQDBBTp3/image.jpg' 147 | }, 148 | { 149 | label: 'Yoda', 150 | className: 'ImageNode', 151 | attributes: { 152 | width:200, 153 | height:200, 154 | fill:"", 155 | stroke:"", 156 | image:'https://konvajs.org/assets/yoda.gif' 157 | }, 158 | url: 'https://konvajs.org/assets/yoda.gif' 159 | } 160 | ]; 161 | 162 | 163 | 164 | export default list; -------------------------------------------------------------------------------- /test/unit-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | 7 | 8 | 9 | 12 | 15 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/unit/Animation-test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { createEditor } from "./test-utils"; 3 | 4 | describe('Animation', function () { 5 | 6 | let editor=createEditor('Animation'); 7 | it('add blink rect node to editor', function () { 8 | 9 | editor.addNode({ 10 | className: 'RectNode', 11 | attributes: { 12 | x: 50, 13 | y: 50, 14 | width: 100, 15 | height: 60, 16 | strokeWidth: 2, 17 | fill:'blue', 18 | stroke: 'red' 19 | } 20 | }) 21 | editor.setAnimation({ 22 | 'type':'blink', 23 | 'autoPlay':true 24 | }) 25 | assert.equal(editor.getSelection().length, 1); 26 | assert.equal(editor.getSelection()[0].animation.type, 'blink'); 27 | }) 28 | it('add rotate rect node to editor', function () { 29 | 30 | editor.addNode({ 31 | className: 'RectNode', 32 | attributes: { 33 | x: 50, 34 | y: 100, 35 | width: 100, 36 | height: 60, 37 | strokeWidth: 2, 38 | fill:'yellow', 39 | stroke: 'red' 40 | } 41 | }) 42 | editor.setAnimation({ 43 | 'type':'rotateByCenter', 44 | 'autoPlay':true 45 | }) 46 | assert.equal(editor.getSelection().length, 1); 47 | assert.equal(editor.getSelection()[0].animation.type, 'rotateByCenter'); 48 | }) 49 | it('add flow line node to editor', function () { 50 | 51 | editor.addNode({ 52 | className: 'LineNode', 53 | attributes: { 54 | x1: 10, 55 | y1: 10, 56 | x2: 200, 57 | y2: 10, 58 | strokeWidth: 2, 59 | stroke: 'red', 60 | dash:[5,5] 61 | } 62 | }) 63 | editor.setAnimation({ 64 | 'type':'flow', 65 | 'autoPlay':true 66 | }) 67 | assert.equal(editor.getSelection().length, 1); 68 | assert.equal(editor.getSelection()[0].animation.type, 'flow'); 69 | }) 70 | it('add path rotate node to editor', function () { 71 | 72 | editor.addNode({ 73 | className: 'LineNode', 74 | attributes: { 75 | x1: 10, 76 | y1: 10, 77 | x2: 200, 78 | y2: 10, 79 | strokeWidth: 2, 80 | stroke: 'red', 81 | dash:[5,5] 82 | } 83 | }) 84 | editor.setAnimation({ 85 | 'type':'rotateByCenter', 86 | 'autoPlay':true 87 | }) 88 | assert.equal(editor.getSelection().length, 1); 89 | assert.equal(editor.getSelection()[0].animation.type, 'flow'); 90 | }) 91 | }) -------------------------------------------------------------------------------- /test/unit/Arc-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { ArcNode } from '../../src/model/ArcNode'; 7 | describe('ArcNode', function () { 8 | 9 | it('createNode', function () { 10 | let editor = createEditor('ArcNode'); 11 | let arcNode = new ArcNode( 12 | { 13 | attributes: { 14 | x: 50, 15 | y: 50, 16 | angle: 50, 17 | innerRadius: 20, 18 | outerRadius: 50, 19 | strokeWidth: 2, 20 | stroke: 'red', 21 | draggable:true 22 | } 23 | } 24 | ); 25 | editor.getDataModel().addNode(arcNode); 26 | assert.equal(arcNode.getAttributeValue('angle'), 50); 27 | 28 | }) 29 | it('add rect node to editor', function () { 30 | 31 | // editor.addNode({ 32 | // className: 'ArcNode', 33 | // attributes: { 34 | // x: 50, 35 | // y: 50, 36 | // angle: 50, 37 | // innerRadius: 20, 38 | // outerRadius: 50, 39 | // strokeWidth: 2, 40 | // stroke: 'red' 41 | // } 42 | // }) 43 | // assert.equal(editor.getSelection().length, 1); 44 | // assert.equal(editor.getSelection()[0].attributes.innerRadius, 20); 45 | }) 46 | 47 | }) 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/unit/Circle-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { CircleNode } from '../../src/model/CircleNode'; 7 | describe('CircleNode', function () { 8 | 9 | it('createNode', function () { 10 | let circleNode=new CircleNode( 11 | { 12 | attributes: { 13 | x: 50, 14 | y: 50, 15 | radius: 50, 16 | strokeWidth: 2, 17 | stroke: 'red' 18 | } 19 | } 20 | ); 21 | assert.equal(circleNode.getAttributeValue('radius'), 50); 22 | 23 | }) 24 | it('add circle node to editor', function () { 25 | let editor=createEditor('CircleNode'); 26 | editor.addNode({ 27 | className: 'CircleNode', 28 | attributes: { 29 | x: 50, 30 | y: 50, 31 | radius: 50, 32 | stroke: 'red' 33 | } 34 | }) 35 | assert.equal(editor.getSelection().length, 1); 36 | }) 37 | }) 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/unit/DataModel-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | import { StarNode } from '../../src/model/StarNode'; 11 | import { TextNode } from '../../src/model/TextNode'; 12 | import { WedgeNode } from '../../src/model/WedgeNode'; 13 | 14 | describe('DataModel', function () { 15 | let editor = createEditor('DataModel'); 16 | let wedgeNode; 17 | let rectNode; 18 | it('create wedge node', function () { 19 | wedgeNode = new WedgeNode( 20 | { 21 | attributes: { 22 | x: 50, 23 | y: 50, 24 | angle: 100, 25 | radius: 15, 26 | clockwise: false, 27 | strokeWidth: 2, 28 | stroke: 'green', 29 | draggable: true 30 | } 31 | } 32 | ); 33 | rectNode = new RectNode( 34 | { 35 | attributes: { 36 | x: 50, 37 | y: 50, 38 | width: 100, 39 | height: 60, 40 | strokeWidth: 2, 41 | stroke: 'red' 42 | } 43 | } 44 | ); 45 | editor?.getDataModel()?.addNode(wedgeNode); 46 | editor?.getDataModel()?.addNode(rectNode); 47 | assert.equal(wedgeNode.getAttributeValue('radius'), 15); 48 | 49 | }) 50 | it('getNodes', function () { 51 | let nodes = editor?.getDataModel()?.getNodes(); 52 | assert.equal(nodes?.length, 2); 53 | 54 | }) 55 | it('getNodeById', function () { 56 | let nodes = editor?.getDataModel()?.getNodes(); 57 | assert.equal(nodes?.length, 2); 58 | 59 | }) 60 | it('toObject', function () { 61 | let obj = editor?.getDataModel()?.toObject(); 62 | console.log(JSON.stringify(obj)); 63 | assert.equal(obj?.nodes.length, 2); 64 | }) 65 | it('onSelectionChanged', function () { 66 | editor?.getDataModel()?.onSelectionChanged(function (sender, event) { 67 | console.log("onSelectionChange", event); 68 | }); 69 | }) 70 | it('onModelChanged', function () { 71 | editor?.getDataModel()?.onModelChanged(function (sender, event) { 72 | console.log("onModelChanged", event); 73 | }); 74 | }) 75 | it('group', function () { 76 | editor?.getDataModel()?.group([wedgeNode, rectNode]); 77 | let nodes = editor?.getDataModel()?.getNodes(); 78 | assert.equal(nodes?.length, 1); 79 | }) 80 | it('unGroup', function () { 81 | let nodes = editor?.getDataModel()?.getNodes(); 82 | if (nodes?.length) { 83 | editor?.getDataModel()?.unGroup([nodes[0]]); 84 | } 85 | 86 | assert.equal(nodes?.length, 2); 87 | }) 88 | // it('saveVariable', function () { 89 | // editor?.getDataModel()?.addVariable('state', { 90 | // defaultVal: 1 91 | // }); 92 | // assert.equal(editor?.getDataModel()?.getVariable('state').defaultVal, 1); 93 | // }) 94 | // it('getVariables', function () { 95 | // assert.equal(editor?.getDataModel()?.getVariables().hasOwnProperty('state'), true); 96 | // }) 97 | // it('setVariables', function () { 98 | // editor?.getDataModel()?.setVariables({ 99 | // 'temperature': { 100 | // defaultVal: 1 101 | // } 102 | // }); 103 | // assert.equal(editor?.getDataModel()?.getVariables().hasOwnProperty('temperature'), true); 104 | // }) 105 | // it('deleteVariable', function () { 106 | // editor?.getDataModel()?.deleteVariable('temperature'); 107 | // assert.equal(editor?.getDataModel()?.getVariables().hasOwnProperty('temperature'), false); 108 | // }) 109 | it('setAttributeValues', function () { 110 | let attrMap = new Map(); 111 | attrMap.set(rectNode, { 'fill': 'yellow' }); 112 | editor?.getDataModel()?.setAttributeValues(attrMap); 113 | assert.equal(rectNode.getAttributeValue('fill'), 'yellow'); 114 | }) 115 | it('addEvent', function () { 116 | editor?.getDataModel()?.addEvent(rectNode, { 117 | type: 'valueUpdate', 118 | action: 'changeProperty', 119 | value: [], 120 | where: { 121 | type: 'none' 122 | } 123 | }); 124 | assert.equal(rectNode.getEvents()[0].type, 'valueUpdate'); 125 | }) 126 | it('updateEvent', function () { 127 | editor?.getDataModel()?.updateEvent(rectNode, { 128 | value: [ 129 | { 130 | name: 'fill', 131 | val: 'red' 132 | } 133 | ], 134 | }, 0); 135 | assert.equal(rectNode.getEvents()[0].value.length, 1); 136 | }) 137 | it('deleteEvent', function () { 138 | editor?.getDataModel()?.deleteEvent(rectNode, 0); 139 | assert.equal(rectNode.getEvents().length, 0); 140 | }) 141 | it('setNodeTag', function () { 142 | editor?.getDataModel()?.setNodeTag(rectNode, "矩形"); 143 | assert.equal(rectNode.getTag(), "矩形"); 144 | }) 145 | }) 146 | 147 | 148 | -------------------------------------------------------------------------------- /test/unit/Ellipse-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | describe('EllipseNode', function () { 7 | 8 | it('createNode', function () { 9 | let ellipseNode=new EllipseNode( 10 | { 11 | attributes: { 12 | x: 50, 13 | y: 50, 14 | radiusX: 50, 15 | radiusY: 60, 16 | strokeWidth: 2, 17 | stroke: 'red' 18 | } 19 | } 20 | ); 21 | assert.equal(ellipseNode.getAttributeValue('radiusX'), 50); 22 | 23 | }) 24 | it('add ellipse node to editor', function () { 25 | let editor=createEditor('EllipseNode'); 26 | editor.addNode({ 27 | className: 'EllipseNode', 28 | attributes: { 29 | x: 50, 30 | y: 50, 31 | width: 50, 32 | height: 60, 33 | strokeWidth: 2, 34 | stroke: 'red' 35 | } 36 | }) 37 | assert.equal(editor.getSelection().length, 1); 38 | assert.equal(editor.getSelection()[0].className, 'EllipseNode'); 39 | }) 40 | }) 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/unit/GraphEditor-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | import { StarNode } from '../../src/model/StarNode'; 11 | import { TextNode } from '../../src/model/TextNode'; 12 | import { WedgeNode } from '../../src/model/WedgeNode'; 13 | import { Node } from '../../src/model/Node'; 14 | 15 | describe('GraphEditor', function () { 16 | let editor = createEditor('GraphEditor'); 17 | let wedgeNode; 18 | let rectNode; 19 | it('add node', function () { 20 | editor.addNode({ 21 | className: 'RectNode', 22 | attributes: { 23 | x: 100, 24 | y: 20, 25 | width: 200, 26 | height: 60, 27 | strokeWidth: 2, 28 | fill: 'gray', 29 | stroke: 'red' 30 | } 31 | }) 32 | editor.addNode({ 33 | className: 'EllipseNode', 34 | attributes: { 35 | x: 100, 36 | y: 30, 37 | radiusX: 50, 38 | radiusY: 25, 39 | strokeWidth: 4, 40 | fill: 'gray', 41 | stroke: 'green' 42 | } 43 | }) 44 | assert.equal(editor.getNodes().length, 2); 45 | 46 | }) 47 | it('copy add paste', function () { 48 | editor.copy(); 49 | editor.paste(); 50 | assert.equal(editor.getNodes().length, 3); 51 | 52 | }) 53 | it('move', function () { 54 | let ids= editor.getNodes().map(item =>item.id); 55 | editor.move('down',30,ids[0]); 56 | assert.equal(editor.getAttributeValue('y',ids[0]), 50); 57 | }) 58 | it('setAttributeValues', function () { 59 | editor.setAttributeValues({ 60 | 'fill':'red' 61 | }); 62 | assert.equal(editor.getSelection()[0].attributes.fill, 'red'); 63 | }) 64 | 65 | it('undo', function () { 66 | editor.undo(); 67 | assert.equal(editor.getSelection()[0].attributes.fill, 'gray'); 68 | }) 69 | it('redo', function () { 70 | editor.redo(); 71 | assert.equal(editor.getSelection()[0].attributes.fill, 'red'); 72 | }) 73 | it('align', function () { 74 | let allNodes:Array= editor.getNodes(); 75 | let ids= allNodes.map(item =>item.id); 76 | editor.align('left',ids); 77 | }) 78 | 79 | it('saveVariable', function () { 80 | let allNodes:Array= editor.getNodes(); 81 | let ids= allNodes.map(item =>item.id); 82 | editor.addVariable('变量1',{ 83 | type:'integer', 84 | defaultVal:0 85 | },false,ids[0]); 86 | editor.addVariable('变量3',{ 87 | type:'integer', 88 | defaultVal:0 89 | },false,ids[0]); 90 | //修改变量 91 | editor.updateVariable('变量2',{ 92 | type:'integer', 93 | defaultVal:0 94 | },false,'变量1',ids[0]); 95 | assert.equal(editor?.getDataModel()?.getNodeById(ids[0]).getVariables().hasOwnProperty('变量3'), true); 96 | }) 97 | it('deleteVariable', function () { 98 | let ids= editor.getNodes().map(item =>item.id); 99 | editor.deleteVariable('变量3',false,ids[0]); 100 | assert.equal(editor?.getDataModel()?.getNodeById(ids[0]).getVariables().hasOwnProperty('变量3'), false); 101 | }) 102 | it('onModelChanged', function () { 103 | editor.onModelChanged(function (sender, event) { 104 | //console.log("onModelChanged", event); 105 | }); 106 | }) 107 | it('group', function () { 108 | let ids= editor.getNodes().map(item =>item.id); 109 | editor.group(ids); 110 | let nodes = editor.getNodes(); 111 | assert.equal(nodes.length, 1); 112 | }) 113 | it('unGroup', function () { 114 | editor.unGroup(); 115 | assert.equal(editor.getNodes().length, 3); 116 | }) 117 | it('setAttributeValue', function () { 118 | let ids= editor.getNodes().map(item =>item.id); 119 | editor.setAttributeValue('fill','yellow',ids[0]); 120 | assert.equal(editor.getAttributeValue('fill',ids[0]), 'yellow'); 121 | }) 122 | it('getAttributes', function () { 123 | let ids= editor.getNodes().map(item =>item.id); 124 | let attrs=editor.getAttributes(ids[0]); 125 | assert.equal(attrs['fill'].value, 'yellow'); 126 | }) 127 | it('addEvent', function () { 128 | let ids= editor.getNodes().map(item =>item.id); 129 | editor.addEvent({ 130 | type: 'valueUpdate', 131 | action: 'changeProperty', 132 | value: [], 133 | where: { 134 | type: 'none' 135 | } 136 | },ids[0]); 137 | assert.equal(editor.getNodes()[0].events[0].action, 'changeProperty'); 138 | }) 139 | // it('addEvent', function () { 140 | // let ids= editor.getNodes().map(item =>item.id); 141 | // editor.addEvent({ 142 | // type:'valueUpdate', 143 | // action:'executeScript', 144 | // fnjs: 'console.log("world")', 145 | // where: { 146 | // type: 'none' 147 | // } 148 | // },ids[0]); 149 | // assert.equal(editor.getNodes()[0].events[0].action, 'changeProperty'); 150 | // }) 151 | it('updateEvent', function () { 152 | let ids= editor.getNodes().map(item =>item.id); 153 | editor.updateEvent({ 154 | type: 'valueUpdate', 155 | action: 'changeProperty', 156 | value: [ 157 | {name:'x',val:'23'} 158 | ], 159 | where: { 160 | type:"comparison", 161 | key:"变量1", 162 | comparison:"=", 163 | value:1 164 | } 165 | },0,ids[0]); 166 | console.log("event is",editor.getNodes()[0].events[0]); 167 | assert.equal(editor.getNodes()[0].events[0].value.length, 1); 168 | }) 169 | it('deleteEvent', function () { 170 | let ids= editor.getNodes().map(item =>item.id); 171 | editor.deleteEvent(0,ids[0]); 172 | assert.equal(editor.getNodes()[0].events.length, 0); 173 | }) 174 | it('toJSON', function () { 175 | let jsonStr=editor.toJSON(); 176 | //console.log(jsonStr); 177 | }) 178 | it('setAnimation', function () { 179 | let ids= editor.getNodes().map(item =>item.id); 180 | //console.log(ids); 181 | editor.setAnimation({ 182 | type:'blink', 183 | autoPlay:true 184 | },ids[0]); 185 | assert.equal(editor.getNodes()[0].animation.type, "blink"); 186 | }) 187 | it('getAllNodeAttributes', function () { 188 | let rectNode=new RectNode(); 189 | let attrs = {}; 190 | let _classes = Node._classes; 191 | for (const _name in _classes) { 192 | let _class = _classes[_name]; 193 | let obj = new _classes[_name](); 194 | let _attrs = obj.attributes; 195 | Object.assign(attrs, _attrs); 196 | } 197 | 198 | }) 199 | }) 200 | 201 | 202 | -------------------------------------------------------------------------------- /test/unit/Group-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { CircleNode } from '../../src/model/CircleNode'; 7 | import { GroupNode } from '../../src/model/GroupNode'; 8 | describe('GroupNode', function () { 9 | let editor=createEditor('GroupNode'); 10 | it('createNode', function () { 11 | let rectNode=new RectNode( 12 | { 13 | attributes: { 14 | x: 50, 15 | y: 50, 16 | width: 100, 17 | height: 60, 18 | strokeWidth: 2, 19 | stroke: 'red' 20 | } 21 | } 22 | ); 23 | let ellipseNode=new EllipseNode( 24 | { 25 | attributes: { 26 | x: 50, 27 | y: 50, 28 | radiusX: 50, 29 | radiusY: 60, 30 | strokeWidth: 2, 31 | stroke: 'red' 32 | } 33 | } 34 | ); 35 | let groupNode=new GroupNode( 36 | { 37 | attributes: { 38 | x: 50, 39 | y: 50, 40 | draggable:true 41 | } 42 | } 43 | ); 44 | groupNode.setMembers([rectNode,ellipseNode]); 45 | editor.getDataModel().addNode(groupNode); 46 | assert.equal(groupNode.getAttributeValue('x'), 50); 47 | 48 | }) 49 | 50 | }) 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/unit/Image-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { ImageNode } from '../../src/model/ImageNode'; 7 | describe('Image', function () { 8 | 9 | 10 | it('add image node to editor', function () { 11 | let editor = createEditor('Image'); 12 | 13 | let imageNode=new ImageNode({ 14 | attributes: { 15 | image: "https://i.postimg.cc/HnFSw-Nf0/image.png", 16 | x: 50, 17 | y: 50, 18 | width: 100, 19 | height: 60, 20 | strokeWidth: 2 21 | } 22 | }) 23 | editor.getDataModel().addNode(imageNode); 24 | assert.equal(imageNode.getAttributeValue('width'),100); 25 | }) 26 | }) 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/unit/Label-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { CircleNode } from '../../src/model/CircleNode'; 7 | import { GroupNode } from '../../src/model/GroupNode'; 8 | import { LabelNode } from '../../src/model/LabelNode'; 9 | describe('LabelNode', function () { 10 | let editor = createEditor('LabelNode'); 11 | it('createNode', function () { 12 | let labelNode = new LabelNode( 13 | { 14 | attributes: { 15 | x: 50, 16 | y: 50, 17 | width: 100, 18 | height: 60, 19 | strokeWidth: 2, 20 | stroke: 'red', 21 | textFill: 'green', 22 | fontSize: 14, 23 | text: '标签测试', 24 | padding: 10 25 | } 26 | } 27 | ); 28 | editor.getDataModel().addNode(labelNode); 29 | assert.equal(labelNode.getAttributeValue('stroke'), 'red'); 30 | }) 31 | }) 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/unit/Line-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { CircleNode } from '../../src/model/CircleNode'; 7 | import { GroupNode } from '../../src/model/GroupNode'; 8 | import { LabelNode } from '../../src/model/LabelNode'; 9 | import { LineNode } from '../../src/model/LineNode'; 10 | import { LineArrowNode } from '../../src/model/LineArrowNode'; 11 | describe('LineNode', function () { 12 | let editor = createEditor('LineNode'); 13 | it('create line Node', function () { 14 | let lineNode = new LineNode( 15 | { 16 | attributes: { 17 | x1: 50, 18 | y1: 50, 19 | x2: 100, 20 | y2: 60, 21 | strokeWidth: 2, 22 | stroke: 'red', 23 | draggable: true 24 | } 25 | } 26 | ); 27 | editor.getDataModel().addNode(lineNode); 28 | assert.equal(lineNode.getAttributeValue('x1'), 50); 29 | }) 30 | it('create line arrow Node', function () { 31 | let lineArrowNode = new LineArrowNode( 32 | { 33 | attributes: { 34 | x1: 80, 35 | y1: 80, 36 | x2: 100, 37 | y2: 100, 38 | strokeWidth: 2, 39 | stroke: 'red', 40 | fill:'red', 41 | draggable: true 42 | } 43 | } 44 | ); 45 | editor.getDataModel().addNode(lineArrowNode); 46 | assert.equal(lineArrowNode.getAttributeValue('x1'), 80); 47 | }) 48 | it('create line arrow Node', function () { 49 | let lineArrowNode = new LineArrowNode( 50 | { 51 | attributes: { 52 | x1: 100, 53 | y1: 150, 54 | x2: 150, 55 | y2: 150, 56 | strokeWidth: 2, 57 | stroke: 'red', 58 | fill:'red', 59 | pointerAtBeginning:true, 60 | draggable: true, 61 | pointerLength:20 62 | } 63 | } 64 | ); 65 | editor.getDataModel().addNode(lineArrowNode); 66 | assert.equal(lineArrowNode.getAttributeValue('x1'), 100); 67 | }) 68 | }) 69 | 70 | 71 | -------------------------------------------------------------------------------- /test/unit/Path-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { CircleNode } from '../../src/model/CircleNode'; 7 | import { PathNode } from '../../src/model/PathNode'; 8 | describe('PathNode', function () { 9 | let editor=createEditor('PathNode'); 10 | it('createNode', function () { 11 | let pathNode=new PathNode( 12 | { 13 | attributes: { 14 | x: 0, 15 | y: 0, 16 | scaleX:0.2, 17 | scaleY:0.2, 18 | data: 'M170.666667 85.333333h682.666666a85.333333 85.333333 0 0 1 85.333334 85.333334v512a85.333333 85.333333 0 0 1-85.333334 85.333333h-170.666666l-170.666667 170.666667-170.666667-170.666667H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333333V170.666667a85.333333 85.333333 0 0 1 85.333334-85.333334m0 85.333334v512h206.08L512 817.92 647.253333 682.666667H853.333333V170.666667H170.666667z', 19 | strokeWidth: 4, 20 | stroke: 'green', 21 | draggable:true 22 | } 23 | } 24 | ); 25 | editor.getDataModel()?.addNode(pathNode); 26 | assert.equal(pathNode.getAttributeValue('x'), 0); 27 | 28 | }) 29 | it('add circle node to editor', function () { 30 | 31 | 32 | }) 33 | }) 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/unit/Pen-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | describe('PenNode', function () { 9 | let editor=createEditor('PenNode'); 10 | it('createNode', function () { 11 | let penNode=new PenNode( 12 | { 13 | attributes: { 14 | x: 0, 15 | y: 0, 16 | points: [23, 20, 23, 160, 70, 93, 150, 109, 290, 139, 270, 93], 17 | strokeWidth: 4, 18 | stroke: 'green', 19 | draggable:true 20 | } 21 | } 22 | ); 23 | editor.getDataModel().addNode(penNode); 24 | assert.equal(penNode.getAttributeValue('x'), 0); 25 | 26 | }) 27 | it('create pen node by drawing', function () { 28 | editor.drawShape('pen'); 29 | 30 | }) 31 | }) 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/unit/Polyline-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | describe('PolylineNode', function () { 10 | let editor=createEditor('PolylineNode'); 11 | it('create polylineNode', function () { 12 | let polylineNode=new PolylineNode( 13 | { 14 | attributes: { 15 | x: 0, 16 | y: 0, 17 | points: [23, 20, 23, 160, 70, 93, 150, 109, 290, 139, 270, 93], 18 | strokeWidth: 4, 19 | stroke: 'green', 20 | draggable:true 21 | } 22 | } 23 | ); 24 | editor.getDataModel().addNode(polylineNode); 25 | assert.equal(polylineNode.getAttributeValue('x'), 0); 26 | 27 | }) 28 | it('create pen node by drawing', function () { 29 | editor.drawShape('polyline'); 30 | 31 | }) 32 | }) 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/unit/Rect-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | describe('RectNode', function () { 7 | let rectNode; 8 | let editor=createEditor('RectNode'); 9 | it('add rect node to editor', function () { 10 | 11 | editor.addNode({ 12 | className: 'RectNode', 13 | attributes: { 14 | x: 50, 15 | y: 50, 16 | width: 100, 17 | height: 60, 18 | strokeWidth: 2, 19 | stroke: 'red' 20 | } 21 | }) 22 | assert.equal(editor.getSelection().length, 1); 23 | assert.equal(editor.getSelection()[0].attributes.width, 100); 24 | }) 25 | it('createNode', function () { 26 | rectNode=new RectNode( 27 | { 28 | attributes: { 29 | x: 50, 30 | y: 50, 31 | width: 100, 32 | height: 60, 33 | strokeWidth: 2, 34 | stroke: 'green', 35 | draggable:true 36 | } 37 | } 38 | ); 39 | editor.getDataModel()?.addNode(rectNode); 40 | assert.equal(rectNode?.getAttributeValue('width'), 100); 41 | 42 | }) 43 | it('rect rotate', function () { 44 | 45 | editor.setAttributeValue('rotation',30); 46 | editor.setAttributeValue('rotation',90); 47 | }) 48 | }) 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/unit/RegularPolygon-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | describe('RegularPolygon', function () { 11 | let editor=createEditor('RegularPolygon'); 12 | it('create regularPolygon', function () { 13 | let regularPolygonNode=new RegularPolygonNode( 14 | { 15 | attributes: { 16 | x: 50, 17 | y: 50, 18 | sides:5, 19 | radius:20, 20 | strokeWidth: 4, 21 | stroke: 'green', 22 | draggable:true 23 | } 24 | } 25 | ); 26 | editor.getDataModel()?.addNode(regularPolygonNode); 27 | assert.equal(regularPolygonNode.getAttributeValue('radius'), 20); 28 | 29 | }) 30 | it('modify attributes', function () { 31 | let regularPolygonNode=new RegularPolygonNode( 32 | { 33 | attributes: { 34 | x: 100, 35 | y: 100, 36 | sides:5, 37 | radius:20, 38 | strokeWidth: 4, 39 | stroke: 'red', 40 | draggable:true 41 | } 42 | } 43 | ); 44 | editor.getDataModel()?.addNode(regularPolygonNode); 45 | regularPolygonNode.setAttributeValue('sides',6); 46 | assert.equal(regularPolygonNode.getAttributeValue('radius'), 20); 47 | 48 | }) 49 | }) 50 | 51 | 52 | -------------------------------------------------------------------------------- /test/unit/Ring-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | import { RingNode } from '../../src/model/RingNode'; 11 | describe('RingNode', function () { 12 | let editor=createEditor('RingNode'); 13 | let ringNode; 14 | it('create ring node', function () { 15 | ringNode=new RingNode( 16 | { 17 | attributes: { 18 | x: 50, 19 | y: 50, 20 | innerRadius:20, 21 | outerRadius:50, 22 | strokeWidth: 4, 23 | stroke: 'green', 24 | draggable:true 25 | } 26 | } 27 | ); 28 | editor.getDataModel().addNode(ringNode); 29 | assert.equal(ringNode.getAttributeValue('innerRadius'), 20); 30 | 31 | }) 32 | it('modify attributes', function () { 33 | ringNode.setAttributeValue('innerRadius',10); 34 | assert.equal(ringNode.getAttributeValue('innerRadius'), 10); 35 | 36 | }) 37 | }) 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/unit/Star-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | import { StarNode } from '../../src/model/StarNode'; 11 | 12 | describe('StarNode', function () { 13 | let editor=createEditor('StarNode'); 14 | let starNode; 15 | it('create star node', function () { 16 | starNode=new StarNode( 17 | { 18 | attributes: { 19 | x: 50, 20 | y: 50, 21 | numPoints:6, 22 | innerRadius:20, 23 | outerRadius:50, 24 | strokeWidth: 4, 25 | stroke: 'green', 26 | draggable:true 27 | } 28 | } 29 | ); 30 | editor.getDataModel().addNode(starNode); 31 | assert.equal(starNode.getAttributeValue('numPoints'), 6); 32 | 33 | }) 34 | it('modify attributes', function () { 35 | starNode.setAttributeValue('numPoints',10); 36 | assert.equal(starNode.getAttributeValue('numPoints'), 10); 37 | 38 | }) 39 | }) 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/unit/Symbol-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import {createEditor} from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | import { SymbolNode } from '../../src/model/SymbolNode'; 11 | 12 | 13 | describe('SymbolNode', function () { 14 | let editor=createEditor('SymbolNode'); 15 | let symbolNode; 16 | it('create symbol node', function () { 17 | let rectNode=new RectNode( 18 | { 19 | attributes: { 20 | x: 50, 21 | y: 50, 22 | width: 100, 23 | height: 60, 24 | strokeWidth: 2, 25 | stroke: 'red' 26 | } 27 | } 28 | ); 29 | let ellipseNode=new EllipseNode( 30 | { 31 | attributes: { 32 | x: 50, 33 | y: 50, 34 | radiusX: 50, 35 | radiusY: 60, 36 | strokeWidth: 2, 37 | stroke: 'red' 38 | } 39 | } 40 | ); 41 | symbolNode=new SymbolNode( 42 | { 43 | attributes: { 44 | x: 50, 45 | y: 50, 46 | draggable:true 47 | } 48 | } 49 | ); 50 | symbolNode.setMembers([rectNode,ellipseNode]); 51 | symbolNode.setSymbolName('breaker'); 52 | editor.getDataModel().addNode(symbolNode); 53 | assert.equal(symbolNode.getSymbolName(), 'breaker'); 54 | 55 | }) 56 | it('modify attributes', function () { 57 | 58 | 59 | }) 60 | }) 61 | 62 | 63 | -------------------------------------------------------------------------------- /test/unit/Text-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import {createEditor} from './test-utils'; 3 | import assert from 'assert'; 4 | import { PathNode } from '../../src/model/PathNode'; 5 | import { PenNode } from '../../src/model/PenNode'; 6 | import { PolylineNode } from '../../src/model/PolylineNode'; 7 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 8 | import { StarNode } from '../../src/model/StarNode'; 9 | import { TextNode } from '../../src/model/TextNode'; 10 | 11 | describe('TextNode', function () { 12 | let editor=createEditor('TextNode'); 13 | let textNode; 14 | it('create text node', function () { 15 | textNode=new TextNode( 16 | { 17 | attributes: { 18 | x: 50, 19 | y: 50, 20 | align:'left', 21 | padding :10, 22 | fontFamily : "Arial", 23 | fontSize : 12, 24 | fontStyle : "normal", 25 | fontVariant : "normal", 26 | text: "中文", 27 | strokeWidth: 2, 28 | stroke: 'green', 29 | draggable:true 30 | } 31 | } 32 | ); 33 | editor.getDataModel()?.addNode(textNode); 34 | assert.equal(textNode.getAttributeValue('padding'), 10); 35 | 36 | }) 37 | it('modify attributes', function () { 38 | textNode.setAttributeValue('padding',5); 39 | assert.equal(textNode.getAttributeValue('padding'), 5); 40 | 41 | }) 42 | }) 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/unit/Utils-test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import {Utils} from "../../src/Utils"; 3 | 4 | describe('Util', function () { 5 | it('getRad', function () { 6 | assert.equal(Utils.getRad(90), 1.5707963267948966) 7 | }) 8 | }) -------------------------------------------------------------------------------- /test/unit/Wedge-test.ts: -------------------------------------------------------------------------------- 1 | import { RectNode } from '../../src/model/RectNode'; 2 | import { EllipseNode } from '../../src/model/EllipseNode'; 3 | import { GraphEditor } from '../../src/GraphEditor'; 4 | import { createEditor } from './test-utils'; 5 | import assert from 'assert'; 6 | import { PathNode } from '../../src/model/PathNode'; 7 | import { PenNode } from '../../src/model/PenNode'; 8 | import { PolylineNode } from '../../src/model/PolylineNode'; 9 | import { RegularPolygonNode } from '../../src/model/RegularPolygonNode'; 10 | import { StarNode } from '../../src/model/StarNode'; 11 | import { TextNode } from '../../src/model/TextNode'; 12 | import { WedgeNode } from '../../src/model/WedgeNode'; 13 | 14 | describe('WedgeNode', function () { 15 | let editor = createEditor('WedgeNode'); 16 | let wedgeNode; 17 | it('create text node', function () { 18 | wedgeNode = new WedgeNode( 19 | { 20 | attributes: { 21 | x: 50, 22 | y: 50, 23 | angle: 30, 24 | radius: 15, 25 | clockwise: false, 26 | strokeWidth: 2, 27 | stroke: 'green', 28 | draggable: true 29 | } 30 | } 31 | ); 32 | editor.getDataModel().addNode(wedgeNode); 33 | assert.equal(wedgeNode.getAttributeValue('radius'), 15); 34 | 35 | }) 36 | it('modify attributes', function () { 37 | wedgeNode.setAttributeValue('radius', 20); 38 | assert.equal(wedgeNode.getAttributeValue('radius'), 20); 39 | 40 | }) 41 | }) 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/unit/test-utils.ts: -------------------------------------------------------------------------------- 1 | import GraphEditor from '../../src/GraphEditor' 2 | export function createEditor(title) { 3 | 4 | var container = 5 | (global.document.createElement('div')) || undefined; 6 | 7 | container.style.width="200px"; 8 | container.style.height="260px"; 9 | container.style.margin="10px"; 10 | container.style.border="1px solid gray"; 11 | container.style.display="flex"; 12 | container.style.flexDirection="column"; 13 | var titleContainer = 14 | (global.document.createElement('div')) || undefined; 15 | titleContainer.innerHTML=title; 16 | var graphContainer = 17 | (global.document.createElement('div')) || undefined; 18 | 19 | container.appendChild(titleContainer); 20 | container.appendChild(graphContainer); 21 | 22 | var editor = new GraphEditor( 23 | { 24 | container:graphContainer, 25 | view: { 26 | size: { 27 | width: 200, 28 | height: 200 29 | } 30 | } 31 | } 32 | ); 33 | document.getElementById("preview")?.appendChild(container); 34 | return editor; 35 | } -------------------------------------------------------------------------------- /tsconfig-cmj.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "cmj", 4 | "module": "CommonJS", 5 | "target": "ES2015", 6 | "noEmitOnError": true, 7 | "lib": ["ES2015", "dom"], 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "removeComments": true 11 | }, 12 | "include": ["./src/**/*.ts"], 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES6", 4 | "target": "ES5", 5 | "noImplicitAny": true, 6 | "noEmit": true, 7 | "strictNullChecks": true, 8 | "removeComments": true, 9 | "preserveConstEnums": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "rootDir": "src", 16 | "baseUrl": "/", 17 | "lib": [ 18 | "ES2017", 19 | "ES2019.array", 20 | "dom" 21 | ], 22 | "downlevelIteration": true 23 | }, 24 | "compileOnSave": true, 25 | "include": [ 26 | "src/**/*" 27 | ], 28 | "exclude": [ 29 | "src/**/*.test.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.all.ts"], 3 | "includes": "src/*.ts", 4 | "out": "docs", 5 | "readme": "none", 6 | "includeVersion": true, 7 | "disableSources": true, 8 | "exclude": [ 9 | "**/node_modules/**" 10 | ], 11 | } 12 | --------------------------------------------------------------------------------