├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── diagram_editor.dart └── src │ ├── abstraction_layer │ ├── policy │ │ ├── base │ │ │ ├── canvas_policy.dart │ │ │ ├── canvas_widgets_policy.dart │ │ │ ├── component_design_policy.dart │ │ │ ├── component_policy.dart │ │ │ ├── component_widgets_policy.dart │ │ │ ├── init_policy.dart │ │ │ ├── link_attachment_policy.dart │ │ │ ├── link_joints_policy.dart │ │ │ ├── link_policy.dart │ │ │ ├── link_widgets_policy.dart │ │ │ └── policy_set.dart │ │ ├── base_policy_set.dart │ │ └── defaults │ │ │ ├── canvas_control_policy.dart │ │ │ ├── link_attachment_crystal_policy.dart │ │ │ ├── link_attachment_oval_policy.dart │ │ │ ├── link_attachment_rect_policy.dart │ │ │ ├── link_control_policy.dart │ │ │ └── link_joint_control_policy.dart │ └── rw │ │ ├── canvas_reader.dart │ │ ├── canvas_writer.dart │ │ ├── model_reader.dart │ │ ├── model_writer.dart │ │ ├── state_reader.dart │ │ └── state_writer.dart │ ├── canvas_context │ ├── canvas_model.dart │ ├── canvas_state.dart │ ├── diagram_editor_context.dart │ └── model │ │ ├── component_data.dart │ │ ├── connection.dart │ │ ├── diagram_data.dart │ │ └── link_data.dart │ ├── utils │ ├── link_style.dart │ ├── painter │ │ ├── delete_icon_painter.dart │ │ ├── grid_painter.dart │ │ ├── link_joint_painter.dart │ │ ├── link_painter.dart │ │ └── rect_highlight_painter.dart │ └── vector_utils.dart │ └── widget │ ├── canvas.dart │ ├── component.dart │ ├── editor.dart │ └── link.dart ├── pubspec.lock ├── pubspec.yaml └── test └── src ├── canvas_context ├── canvas_model_test.dart ├── canvas_state_test.dart └── model │ ├── component_data_test.dart │ └── link_data_test.dart └── widget ├── canvas_test.dart ├── component_test.dart └── link_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 60bd88df915880d23877bfc1602e8ddcf4c4dd2a 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Example", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "example/main.dart", 12 | }, 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/flutter_sdk", 3 | // Remove .fvm files from search 4 | "search.exclude": { 5 | "**/.fvm": true 6 | }, 7 | // Remove from file watching 8 | "files.watcherExclude": { 9 | "**/.fvm": true 10 | }, 11 | "explorer.fileNesting.patterns": { 12 | "pubspec.yaml": "pubspec.lock,pubspec.semver.js, analysis_options.yaml,dart_test.yaml", 13 | "README.md": "CHANGELOG.md", 14 | "*.dart": "${capture}.freezed.dart, ${capture}.g.dart", 15 | }, 16 | "explorer.fileNesting.enabled": true, 17 | "explorer.fileNesting.expand": false, 18 | "[dart]": { 19 | "editor.formatOnSave": true, 20 | "editor.formatOnType": true, 21 | "editor.rulers": [ 22 | 80 23 | ], 24 | "editor.formatOnPaste": true, 25 | "editor.defaultFormatter": "Dart-Code.dart-code", 26 | "editor.codeActionsOnSave": { 27 | "source.organizeImports": "explicit", 28 | "source.fixAll": "explicit" 29 | } 30 | }, 31 | "editor.foldingImportsByDefault": true, 32 | "dart.lineLength": 80, 33 | "git.branchPrefix": "features/", 34 | "git.branchProtection": [ 35 | "main", 36 | "releases/*" 37 | ], 38 | "git.branchRandomName.dictionary": [ 39 | "adjectives", 40 | "animals", 41 | "colors" 42 | ], 43 | "git.branchRandomName.enable": true, 44 | "dart.previewFlutterUiGuides": true, 45 | "dart.previewFlutterUiGuidesCustomTracking": true, 46 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 0.2.3 3 | 4 | * Change color serialization to Json approach (thanks pklosek) 5 | 6 | # 0.2.2 7 | 8 | * Update SDK to `sdk: ">=3.5.0 <4.0.0"` 9 | * Fix: new static analysis 10 | 11 | # 0.2.1 12 | 13 | * Fix: dart format 14 | 15 | # 0.2.0 16 | 17 | * Updated to dart >=3.0.0 18 | * Updated dependencies 19 | * Added linter rules and project formatted. 20 | * Demo app example code updated. 21 | 22 | * Fixed example 23 | * Added `showLinksOnTopOfComponents` option to canvas_policy. 24 | 25 | # 0.1.9 26 | 27 | * Fixed example 28 | * Added `showLinksOnTopOfComponents` option to canvas_policy. 29 | 30 | # 0.1.8 31 | 32 | * Update dependencies. 33 | 34 | # 0.1.7 35 | 36 | * Update dependencies. 37 | 38 | # 0.1.6 39 | 40 | * Update dependencies. 41 | 42 | # 0.1.5 43 | 44 | * Run flutter format again. 45 | 46 | # 0.1.4 47 | 48 | * Update dependencies. 49 | * Flutter format to get max pub.dev points. 50 | 51 | # 0.1.3 52 | 53 | * Fix flutter analysis issues. 54 | 55 | # 0.1.2 56 | 57 | * Add `serializeDiagram()` and `deserializeDiagram()` functions to allow saving the diagram. 58 | * Update dependencies. 59 | * Example update -- it should now explain how to use the serialization. 60 | 61 | # 0.1.1 62 | 63 | * Format code. 64 | 65 | # 0.1.0 66 | 67 | * ### Migrate to null-safety. 68 | 69 | * Adds componentExists function. 70 | * Adds linkExists function. 71 | 72 | # 0.0.12 73 | 74 | * Update dependencies in pubspec.yaml file. 75 | 76 | # 0.0.11 77 | 78 | * Update pubspec.yaml file. 79 | 80 | # 0.0.10 81 | 82 | * Fix remove component with children test. 83 | 84 | # 0.0.9 85 | 86 | * Add documentation comments to reader/writer. 87 | * Move custom components widgets from Canvas to Component. 88 | 89 | # 0.0.8 90 | 91 | * Add some more documentation comments. 92 | * Fix documentation comments. 93 | 94 | # 0.0.7 95 | 96 | * Add more documentation comments. 97 | * Fix readme. 98 | 99 | # 0.0.6 100 | 101 | * Add documentation comments. 102 | * Update the example and add comments to it. 103 | * Update readme file. 104 | * Add some asserts ("the model must contain this id"). 105 | 106 | # 0.0.5 107 | 108 | * Fix add/remove component parent. 109 | * Prevent parent-child loops. 110 | 111 | # 0.0.4 112 | 113 | * Other link attachment policies. 114 | 115 | # 0.0.3 116 | 117 | * Fix example. 118 | 119 | # 0.0.2 120 | 121 | * Add example. 122 | * Update dependencies. 123 | * Description in yaml. 124 | 125 | # 0.0.1 126 | 127 | * Initial release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Arokip 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diagram_editor 2 | 3 | [![pub package](https://img.shields.io/pub/v/diagram_editor.svg)](https://pub.dev/packages/diagram_editor) 4 | 5 | Flutter diagram editor library for showing and editing diagrams of custom type. It provides DiagramEditor widget and a possibility to customize all editor design and behavior. 6 | 7 | 8 | 9 | [Demo App Example](https://arokip.github.io/fdl_demo_app) ([example source code](https://github.com/Arokip/fdl_demo_app)) 10 | 11 | 12 | ## Getting Started 13 | 14 | Use of `DiagramEditor` widget: 15 | 16 | ``` 17 | DiagramEditor( 18 | diagramEditorContext: DiagramEditorContext( 19 | policySet: myPolicySet, 20 | ), 21 | ), 22 | ``` 23 | 24 | `myPolicySet` is a class composed of mixins, for example: 25 | 26 | ``` 27 | class MyPolicySet extends PolicySet 28 | with 29 | MyInitPolicy, 30 | CanvasControlPolicy, 31 | LinkControlPolicy, 32 | LinkJointControlPolicy, 33 | LinkAttachmentRectPolicy {} 34 | ``` 35 | 36 | `MyInitpolicy` can be following: 37 | 38 | ``` 39 | mixin MyInitPolicy implements InitPolicy { 40 | @override 41 | initializeDiagramEditor() { 42 | canvasWriter.state.setCanvasColor(Colors.grey); 43 | } 44 | } 45 | ``` 46 | 47 | For example in `MyCanvasPolicy` in function `onCanvasTapUp(TapUpDetails details)` a new component is added if no component is selected. 48 | 49 | ``` 50 | mixin MyCanvasPolicy implements CanvasPolicy, CustomPolicy { 51 | @override 52 | onCanvasTapUp(TapUpDetails details) async { 53 | canvasWriter.model.hideAllLinkJoints(); 54 | if (selectedComponentId != null) { 55 | hideComponentHighlight(selectedComponentId); 56 | } else { 57 | canvasWriter.model.addComponent( 58 | ComponentData( 59 | size: Size(96, 72), 60 | position: canvasReader.state.fromCanvasCoordinates(details.localPosition), 61 | data: MyComponentData(), 62 | ), 63 | ); 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | There are several editor policies that can be implemented and added to the policy set: 70 | - `InitPolicy` 71 | - `CanvasPolicy` 72 | - `ComponentPolicy` 73 | - `ComponentDesignPolicy` 74 | - `LinkPolicy` 75 | - `LinkJointPolicy` 76 | - `LinkAttachmentPolicy` 77 | - `LinkWidgetsPolicy` 78 | - `CanvasWidgetsPolicy` 79 | - `ComponentWidgetsPolicy` 80 | 81 | Some policies are already implemented and ready to use: 82 | - `CanvasControlPolicy` 83 | - `LinkControlPolicy` 84 | - `LinkJointControlPolicy` 85 | - `LinkAttachmentRectPolicy` 86 | 87 | Possibilities of usage of individual policies are described in the documentation. 88 | More in examples (links above). 89 | 90 | 91 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: ["**/*.g.dart", "lib/generated/**"] 5 | 6 | linter: 7 | rules: 8 | always_declare_return_types: true 9 | avoid_multiple_declarations_per_line: true 10 | prefer_final_in_for_each: true 11 | unnecessary_lambdas: true 12 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:diagram_editor/diagram_editor.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | void main() => runApp(const DiagramApp()); 7 | 8 | class DiagramApp extends StatefulWidget { 9 | const DiagramApp({super.key}); 10 | 11 | @override 12 | DiagramAppState createState() => DiagramAppState(); 13 | } 14 | 15 | class DiagramAppState extends State { 16 | MyPolicySet myPolicySet = MyPolicySet(); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return MaterialApp( 21 | home: Scaffold( 22 | body: SafeArea( 23 | child: Stack( 24 | children: [ 25 | const ColoredBox(color: Colors.grey), 26 | Padding( 27 | padding: const EdgeInsets.all(16), 28 | child: DiagramEditor( 29 | diagramEditorContext: 30 | DiagramEditorContext(policySet: myPolicySet), 31 | ), 32 | ), 33 | Padding( 34 | padding: const EdgeInsets.all(4), 35 | child: Row( 36 | children: [ 37 | ElevatedButton( 38 | onPressed: () => myPolicySet.deleteAllComponents(), 39 | style: 40 | ElevatedButton.styleFrom(backgroundColor: Colors.red), 41 | child: const Text('delete all'), 42 | ), 43 | const Spacer(), 44 | ElevatedButton( 45 | onPressed: () => myPolicySet.serialize(), 46 | child: const Text('serialize'), 47 | ), 48 | const SizedBox(width: 8), 49 | ElevatedButton( 50 | onPressed: () => myPolicySet.deserialize(), 51 | child: const Text('deserialize'), 52 | ), 53 | ], 54 | ), 55 | ), 56 | ], 57 | ), 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | 64 | // Custom component Data which you can assign to a component to dynamic data property. 65 | class MyComponentData { 66 | MyComponentData(); 67 | 68 | bool isHighlightVisible = false; 69 | Color color = Color((math.Random().nextDouble() * 0xFFFFFF).toInt()) 70 | .withValues(alpha: 1.0); 71 | 72 | void showHighlight() { 73 | isHighlightVisible = true; 74 | } 75 | 76 | void hideHighlight() { 77 | isHighlightVisible = false; 78 | } 79 | 80 | // Function used to deserialize the diagram. Must be passed to `canvasWriter.model.deserializeDiagram` for proper deserialization. 81 | MyComponentData.fromJson(Map json) 82 | : isHighlightVisible = json['highlight'], 83 | color = Color(int.parse(json['color'], radix: 16)); 84 | 85 | // Function used to serialization of the diagram. E.g. to save to a file. 86 | Map toJson() => { 87 | 'highlight': isHighlightVisible, 88 | 'color': (((color.a * 255).round() << 24) | 89 | ((color.r * 255).round() << 16) | 90 | ((color.g * 255).round() << 8) | 91 | ((color.b * 255).round())) 92 | .toRadixString(16), 93 | }; 94 | } 95 | 96 | // A set of policies compound of mixins. There are some custom policy implementations and some policies defined by diagram_editor library. 97 | class MyPolicySet extends PolicySet 98 | with 99 | MyInitPolicy, 100 | MyComponentDesignPolicy, 101 | MyCanvasPolicy, 102 | MyComponentPolicy, 103 | CustomPolicy, 104 | // 105 | CanvasControlPolicy, 106 | LinkControlPolicy, 107 | LinkJointControlPolicy, 108 | LinkAttachmentRectPolicy {} 109 | 110 | // A place where you can init the canvas or your diagram (eg. load an existing diagram). 111 | mixin MyInitPolicy implements InitPolicy { 112 | @override 113 | void initializeDiagramEditor() { 114 | canvasWriter.state.setCanvasColor(Colors.grey[300]!); 115 | } 116 | } 117 | 118 | // This is the place where you can design a component. 119 | // Use switch on componentData.type or componentData.data to define different component designs. 120 | mixin MyComponentDesignPolicy implements ComponentDesignPolicy { 121 | @override 122 | Widget showComponentBody(ComponentData componentData) { 123 | return Container( 124 | decoration: BoxDecoration( 125 | color: (componentData.data as MyComponentData).color, 126 | border: Border.all( 127 | width: 2, 128 | color: (componentData.data as MyComponentData).isHighlightVisible 129 | ? Colors.pink 130 | : Colors.black, 131 | ), 132 | ), 133 | child: const Center(child: Text('component')), 134 | ); 135 | } 136 | } 137 | 138 | // You can override the behavior of any gesture on canvas here. 139 | // Note that it also implements CustomPolicy where own variables and functions can be defined and used here. 140 | mixin MyCanvasPolicy implements CanvasPolicy, CustomPolicy { 141 | @override 142 | void onCanvasTapUp(TapUpDetails details) { 143 | canvasWriter.model.hideAllLinkJoints(); 144 | if (selectedComponentId != null) { 145 | hideComponentHighlight(selectedComponentId); 146 | } else { 147 | canvasWriter.model.addComponent( 148 | ComponentData( 149 | size: const Size(96, 72), 150 | position: 151 | canvasReader.state.fromCanvasCoordinates(details.localPosition), 152 | data: MyComponentData(), 153 | ), 154 | ); 155 | } 156 | } 157 | } 158 | 159 | // Mixin where component behaviour is defined. In this example it is the movement, highlight and connecting two components. 160 | mixin MyComponentPolicy implements ComponentPolicy, CustomPolicy { 161 | // variable used to calculate delta offset to move the component. 162 | late Offset lastFocalPoint; 163 | 164 | @override 165 | void onComponentTap(String componentId) { 166 | canvasWriter.model.hideAllLinkJoints(); 167 | 168 | bool connected = connectComponents(selectedComponentId, componentId); 169 | hideComponentHighlight(selectedComponentId); 170 | if (!connected) { 171 | highlightComponent(componentId); 172 | } 173 | } 174 | 175 | @override 176 | void onComponentLongPress(String componentId) { 177 | hideComponentHighlight(selectedComponentId); 178 | canvasWriter.model.hideAllLinkJoints(); 179 | canvasWriter.model.removeComponent(componentId); 180 | } 181 | 182 | @override 183 | void onComponentScaleStart(componentId, details) { 184 | lastFocalPoint = details.localFocalPoint; 185 | } 186 | 187 | @override 188 | void onComponentScaleUpdate(componentId, details) { 189 | Offset positionDelta = details.localFocalPoint - lastFocalPoint; 190 | canvasWriter.model.moveComponent(componentId, positionDelta); 191 | lastFocalPoint = details.localFocalPoint; 192 | } 193 | 194 | // This function tests if it's possible to connect the components and if yes, connects them 195 | bool connectComponents(String? sourceComponentId, String? targetComponentId) { 196 | if (sourceComponentId == null || targetComponentId == null) { 197 | return false; 198 | } 199 | // tests if the ids are not same (the same component) 200 | if (sourceComponentId == targetComponentId) { 201 | return false; 202 | } 203 | // tests if the connection between two components already exists (one way) 204 | if (canvasReader.model.getComponent(sourceComponentId).connections.any( 205 | (connection) => 206 | (connection is ConnectionOut) && 207 | (connection.otherComponentId == targetComponentId), 208 | )) { 209 | return false; 210 | } 211 | 212 | // This connects two components (creates a link between), you can define the design of the link with LinkStyle. 213 | canvasWriter.model.connectTwoComponents( 214 | sourceComponentId: sourceComponentId, 215 | targetComponentId: targetComponentId, 216 | linkStyle: LinkStyle( 217 | arrowType: ArrowType.pointedArrow, 218 | lineWidth: 1.5, 219 | backArrowType: ArrowType.centerCircle, 220 | ), 221 | ); 222 | 223 | return true; 224 | } 225 | } 226 | 227 | // You can create your own Policy to define own variables and functions with canvasReader and canvasWriter. 228 | mixin CustomPolicy implements PolicySet { 229 | String? selectedComponentId; 230 | String serializedDiagram = '{"components": [], "links": []}'; 231 | 232 | void highlightComponent(String componentId) { 233 | canvasReader.model.getComponent(componentId).data.showHighlight(); 234 | canvasReader.model.getComponent(componentId).updateComponent(); 235 | selectedComponentId = componentId; 236 | } 237 | 238 | void hideComponentHighlight(String? componentId) { 239 | if (componentId != null) { 240 | canvasReader.model.getComponent(componentId).data.hideHighlight(); 241 | canvasReader.model.getComponent(componentId).updateComponent(); 242 | selectedComponentId = null; 243 | } 244 | } 245 | 246 | void deleteAllComponents() { 247 | selectedComponentId = null; 248 | canvasWriter.model.removeAllComponents(); 249 | } 250 | 251 | // Save the diagram to String in json format. 252 | void serialize() { 253 | serializedDiagram = canvasReader.model.serializeDiagram(); 254 | } 255 | 256 | // Load the diagram from json format. Do it cautiously, to prevent unstable state remove the previous diagram (id collision can happen). 257 | void deserialize() { 258 | canvasWriter.model.removeAllComponents(); 259 | canvasWriter.model.deserializeDiagram( 260 | serializedDiagram, 261 | decodeCustomComponentData: MyComponentData.fromJson, 262 | decodeCustomLinkData: null, 263 | ); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /lib/diagram_editor.dart: -------------------------------------------------------------------------------- 1 | export 'src/abstraction_layer/policy/base/canvas_policy.dart'; 2 | export 'src/abstraction_layer/policy/base/canvas_widgets_policy.dart'; 3 | export 'src/abstraction_layer/policy/base/component_design_policy.dart'; 4 | export 'src/abstraction_layer/policy/base/component_policy.dart'; 5 | export 'src/abstraction_layer/policy/base/component_widgets_policy.dart'; 6 | export 'src/abstraction_layer/policy/base/init_policy.dart'; 7 | export 'src/abstraction_layer/policy/base/link_attachment_policy.dart'; 8 | export 'src/abstraction_layer/policy/base/link_joints_policy.dart'; 9 | export 'src/abstraction_layer/policy/base/link_policy.dart'; 10 | export 'src/abstraction_layer/policy/base/link_widgets_policy.dart'; 11 | export 'src/abstraction_layer/policy/base/policy_set.dart'; 12 | export 'src/abstraction_layer/policy/defaults/canvas_control_policy.dart'; 13 | export 'src/abstraction_layer/policy/defaults/link_attachment_rect_policy.dart'; 14 | export 'src/abstraction_layer/policy/defaults/link_attachment_oval_policy.dart'; 15 | export 'src/abstraction_layer/policy/defaults/link_attachment_crystal_policy.dart'; 16 | export 'src/abstraction_layer/policy/defaults/link_control_policy.dart'; 17 | export 'src/abstraction_layer/policy/defaults/link_joint_control_policy.dart'; 18 | export 'src/canvas_context/diagram_editor_context.dart'; 19 | export 'src/canvas_context/model/component_data.dart'; 20 | export 'src/canvas_context/model/connection.dart'; 21 | export 'src/canvas_context/model/link_data.dart'; 22 | export 'src/utils/link_style.dart'; 23 | export 'src/utils/painter/grid_painter.dart'; 24 | export 'src/utils/painter/rect_highlight_painter.dart'; 25 | export 'src/utils/vector_utils.dart'; 26 | export 'src/widget/editor.dart'; 27 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/canvas_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Allows you to define the canvas behaviour on any gesture registered by the [Canvas]. 6 | mixin CanvasPolicy on BasePolicySet { 7 | void onCanvasTap() {} 8 | 9 | void onCanvasTapDown(TapDownDetails details) {} 10 | 11 | void onCanvasTapUp(TapUpDetails details) {} 12 | 13 | void onCanvasTapCancel() {} 14 | 15 | void onCanvasLongPress() {} 16 | 17 | void onCanvasScaleStart(ScaleStartDetails details) {} 18 | 19 | void onCanvasScaleUpdate(ScaleUpdateDetails details) {} 20 | 21 | void onCanvasScaleEnd(ScaleEndDetails details) {} 22 | 23 | void onCanvasLongPressStart(LongPressStartDetails details) {} 24 | 25 | void onCanvasLongPressMoveUpdate(LongPressMoveUpdateDetails details) {} 26 | 27 | void onCanvasLongPressEnd(LongPressEndDetails details) {} 28 | 29 | void onCanvasLongPressUp() {} 30 | 31 | void onCanvasPointerSignal(PointerSignalEvent event) {} 32 | 33 | bool get showLinksOnTopOfComponents => true; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/canvas_widgets_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// Allows you to add any widget to the canvas. 5 | mixin CanvasWidgetsPolicy on BasePolicySet { 6 | /// Allows you to add any widget to the canvas. 7 | /// 8 | /// The widgets will be displayed under all components and links. 9 | /// 10 | /// Recommendation: use Positioned as the root widget. 11 | List showCustomWidgetsOnCanvasBackground(BuildContext context) { 12 | return []; 13 | } 14 | 15 | /// Allows you to add any widget to the canvas. 16 | /// 17 | /// The widgets will be displayed over all components and links. 18 | /// 19 | /// Recommendation: use Positioned as the root widget. 20 | List showCustomWidgetsOnCanvasForeground(BuildContext context) { 21 | return []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/component_design_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Allows you to specify a design of the components. 6 | mixin ComponentDesignPolicy on BasePolicySet { 7 | /// Returns a widget that specifies a design of this component. 8 | /// 9 | /// Recommendation: type can by used to determine what widget should be returned. 10 | Widget? showComponentBody(ComponentData componentData) { 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/component_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | 4 | /// Allows you to define the component behaviour on any gesture registered by the [Component]. 5 | mixin ComponentPolicy on BasePolicySet { 6 | void onComponentTap(String componentId) {} 7 | 8 | void onComponentTapDown(String componentId, TapDownDetails details) {} 9 | 10 | void onComponentTapUp(String componentId, TapUpDetails details) {} 11 | 12 | void onComponentTapCancel(String componentId) {} 13 | 14 | void onComponentScaleStart(String componentId, ScaleStartDetails details) {} 15 | 16 | void onComponentScaleUpdate(String componentId, ScaleUpdateDetails details) {} 17 | 18 | void onComponentScaleEnd(String componentId, ScaleEndDetails details) {} 19 | 20 | void onComponentLongPress(String componentId) {} 21 | 22 | void onComponentLongPressStart( 23 | String componentId, LongPressStartDetails details) {} 24 | 25 | void onComponentLongPressMoveUpdate( 26 | String componentId, LongPressMoveUpdateDetails details) {} 27 | 28 | void onComponentLongPressEnd( 29 | String componentId, LongPressEndDetails details) {} 30 | 31 | void onComponentLongPressUp(String componentId) {} 32 | 33 | void onComponentPointerSignal(String componentId, PointerSignalEvent event) {} 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/component_widgets_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Allows you to add any widget to a component. 6 | mixin ComponentWidgetsPolicy on BasePolicySet { 7 | /// Allows you to add any widget to a component. 8 | /// 9 | /// These widgets will be displayed under all components. 10 | /// 11 | /// You have [ComponentData] here so you can customize the widgets to individual component. 12 | Widget showCustomWidgetWithComponentDataUnder( 13 | BuildContext context, 14 | ComponentData componentData, 15 | ) { 16 | return const SizedBox.shrink(); 17 | } 18 | 19 | /// Allows you to add any widget to a component. 20 | /// 21 | /// These widgets will have the same z-order as this component and will be displayed over this component. 22 | /// 23 | /// You have [ComponentData] here so you can customize the widgets to individual component. 24 | Widget showCustomWidgetWithComponentData( 25 | BuildContext context, 26 | ComponentData componentData, 27 | ) { 28 | return const SizedBox.shrink(); 29 | } 30 | 31 | /// Allows you to add any widget to a component. 32 | /// 33 | /// These widgets will be displayed over all components. 34 | /// 35 | /// You have [ComponentData] here so you can customize the widgets to individual component. 36 | Widget showCustomWidgetWithComponentDataOver( 37 | BuildContext context, 38 | ComponentData componentData, 39 | ) { 40 | return const SizedBox.shrink(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/init_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | 3 | /// Allows you to prepare canvas before anything. 4 | mixin InitPolicy on BasePolicySet { 5 | /// Allows you to prepare diagram editor before anything. 6 | /// 7 | /// It's possible to change canvas state here. Or load a diagram. 8 | void initializeDiagramEditor() {} 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/link_attachment_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | mixin LinkAttachmentPolicy on BasePolicySet { 6 | /// Calculates an alignment of link endpoint on a component from ComponentData and targetPoint (nearest link point from this component). 7 | /// 8 | /// With no implementation the link will attach to center of the component. 9 | Alignment getLinkEndpointAlignment( 10 | ComponentData componentData, 11 | Offset targetPoint, 12 | ) { 13 | return Alignment.center; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/link_joints_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// Allows you to define the link's joint behaviour on any gesture registered by the link's joint. 5 | mixin LinkJointPolicy on BasePolicySet { 6 | void onLinkJointTap(int jointIndex, String linkId) {} 7 | 8 | void onLinkJointTapDown( 9 | int jointIndex, String linkId, TapDownDetails details) {} 10 | 11 | void onLinkJointTapUp(int jointIndex, String linkId, TapUpDetails details) {} 12 | 13 | void onLinkJointTapCancel(int jointIndex, String linkId) {} 14 | 15 | void onLinkJointScaleStart( 16 | int jointIndex, String linkId, ScaleStartDetails details) {} 17 | 18 | void onLinkJointScaleUpdate( 19 | int jointIndex, String linkId, ScaleUpdateDetails details) {} 20 | 21 | void onLinkJointScaleEnd( 22 | int jointIndex, String linkId, ScaleEndDetails details) {} 23 | 24 | void onLinkJointLongPress(int jointIndex, String linkId) {} 25 | 26 | void onLinkJointLongPressStart( 27 | int jointIndex, String linkId, LongPressStartDetails details) {} 28 | 29 | void onLinkJointLongPressMoveUpdate( 30 | int jointIndex, String linkId, LongPressMoveUpdateDetails details) {} 31 | 32 | void onLinkJointLongPressEnd( 33 | int jointIndex, String linkId, LongPressEndDetails details) {} 34 | 35 | void onLinkJointLongPressUp(int jointIndex, String linkId) {} 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/link_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | 4 | /// Allows you to define the link behaviour on any gesture registered by the [Link]. 5 | mixin LinkPolicy on BasePolicySet { 6 | void onLinkTap(String linkId) {} 7 | 8 | void onLinkTapDown(String linkId, TapDownDetails details) {} 9 | 10 | void onLinkTapUp(String linkId, TapUpDetails details) {} 11 | 12 | void onLinkTapCancel(String linkId) {} 13 | 14 | void onLinkScaleStart(String linkId, ScaleStartDetails details) {} 15 | 16 | void onLinkScaleUpdate(String linkId, ScaleUpdateDetails details) {} 17 | 18 | void onLinkScaleEnd(String linkId, ScaleEndDetails details) {} 19 | 20 | void onLinkLongPress(String linkId) {} 21 | 22 | void onLinkLongPressStart(String linkId, LongPressStartDetails details) {} 23 | 24 | void onLinkLongPressMoveUpdate( 25 | String linkId, LongPressMoveUpdateDetails details) {} 26 | 27 | void onLinkLongPressEnd(String linkId, LongPressEndDetails details) {} 28 | 29 | void onLinkLongPressUp(String linkId) {} 30 | 31 | void onLinkPointerSignal(String linkId, PointerSignalEvent event) {} 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/link_widgets_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/link_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Allows you to add any widget to a link. 6 | mixin LinkWidgetsPolicy on BasePolicySet { 7 | /// Allows you to add any widget to a link. 8 | /// 9 | /// You have [LinkData] here so you can customize the widgets to individual link. 10 | /// 11 | /// Recommendation: use Positioned as the root widget. 12 | List showWidgetsWithLinkData( 13 | BuildContext context, 14 | LinkData linkData, 15 | ) { 16 | return []; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base/policy_set.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/diagram_editor.dart'; 2 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 3 | 4 | /// Fundamental policy set. Your policy set should extend [PolicySet]. 5 | class PolicySet extends BasePolicySet 6 | with 7 | InitPolicy, 8 | CanvasPolicy, 9 | ComponentPolicy, 10 | ComponentDesignPolicy, 11 | LinkPolicy, 12 | LinkJointPolicy, 13 | LinkAttachmentPolicy, 14 | LinkWidgetsPolicy, 15 | CanvasWidgetsPolicy, 16 | ComponentWidgetsPolicy {} 17 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/base_policy_set.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/rw/canvas_reader.dart'; 2 | import 'package:diagram_editor/src/abstraction_layer/rw/canvas_writer.dart'; 3 | 4 | class BasePolicySet { 5 | /// Allows you to read all data from diagram/canvas model. 6 | late CanvasReader canvasReader; 7 | 8 | /// Allows you to change diagram/canvas model data. 9 | late CanvasWriter canvasWriter; 10 | 11 | /// Initialize policy in [DiagramEditorContext]. 12 | void initializePolicy(CanvasReader canvasReader, CanvasWriter canvasWriter) { 13 | this.canvasReader = canvasReader; 14 | this.canvasWriter = canvasWriter; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/defaults/canvas_control_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base_policy_set.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Optimized implementation of [CanvasPolicy]. 6 | /// 7 | /// It enabled pan and zoom of the canvas. 8 | /// 9 | /// It uses [onCanvasScaleStart], [onCanvasScaleUpdate], [onCanvasScaleEnd], [onCanvasPointerSignal]. 10 | /// Feel free to override other functions from [CanvasPolicy] and add them to [PolicySet]. 11 | mixin CanvasControlPolicy on BasePolicySet { 12 | AnimationController? _animationController; 13 | double _baseScale = 1.0; 14 | Offset _basePosition = const Offset(0, 0); 15 | 16 | Offset _lastFocalPoint = const Offset(0, 0); 17 | 18 | Offset transformPosition = const Offset(0, 0); 19 | double transformScale = 1.0; 20 | 21 | bool canUpdateCanvasModel = false; 22 | 23 | AnimationController? getAnimationController() { 24 | return _animationController; 25 | } 26 | 27 | void setAnimationController(AnimationController animationController) { 28 | _animationController = animationController; 29 | } 30 | 31 | void disposeAnimationController() { 32 | _animationController?.dispose(); 33 | } 34 | 35 | void onCanvasScaleStart(ScaleStartDetails details) { 36 | _baseScale = canvasReader.state.scale; 37 | _basePosition = canvasReader.state.position; 38 | 39 | _lastFocalPoint = details.focalPoint; 40 | } 41 | 42 | void onCanvasScaleUpdate(ScaleUpdateDetails details) { 43 | if (canUpdateCanvasModel) { 44 | _animationController?.repeat(); 45 | _updateCanvasModelWithLastValues(); 46 | 47 | double previousScale = transformScale; 48 | 49 | transformPosition += details.focalPoint - _lastFocalPoint; 50 | transformScale = keepScaleInBounds(details.scale, _baseScale); 51 | 52 | var focalPoint = (details.localFocalPoint - transformPosition); 53 | var focalPointScaled = focalPoint * (transformScale / previousScale); 54 | 55 | _lastFocalPoint = details.focalPoint; 56 | 57 | transformPosition += focalPoint - focalPointScaled; 58 | 59 | _animationController?.reset(); 60 | } 61 | } 62 | 63 | void onCanvasScaleEnd(ScaleEndDetails details) { 64 | if (canUpdateCanvasModel) { 65 | _updateCanvasModelWithLastValues(); 66 | } 67 | 68 | _animationController?.reset(); 69 | 70 | transformPosition = const Offset(0, 0); 71 | transformScale = 1.0; 72 | 73 | canvasWriter.state.updateCanvas(); 74 | } 75 | 76 | void _updateCanvasModelWithLastValues() { 77 | canvasWriter.state 78 | .setPosition((_basePosition * transformScale) + transformPosition); 79 | canvasWriter.state.setScale(transformScale * _baseScale); 80 | canUpdateCanvasModel = false; 81 | } 82 | 83 | void onCanvasPointerSignal(PointerSignalEvent event) { 84 | if (event is PointerScrollEvent) { 85 | double scaleChange = event.scrollDelta.dy < 0 86 | ? (1 / canvasReader.state.mouseScaleSpeed) 87 | : (canvasReader.state.mouseScaleSpeed); 88 | 89 | scaleChange = keepScaleInBounds(scaleChange, canvasReader.state.scale); 90 | 91 | if (scaleChange == 0.0) { 92 | return; 93 | } 94 | 95 | double previousScale = canvasReader.state.scale; 96 | 97 | canvasWriter.state.updateScale(scaleChange); 98 | 99 | var focalPoint = (event.localPosition - canvasReader.state.position); 100 | var focalPointScaled = 101 | focalPoint * (canvasReader.state.scale / previousScale); 102 | 103 | canvasWriter.state.updatePosition(focalPoint - focalPointScaled); 104 | canvasWriter.state.updateCanvas(); 105 | } 106 | } 107 | 108 | double keepScaleInBounds(double scale, double canvasScale) { 109 | double scaleResult = scale; 110 | if (scale * canvasScale <= canvasReader.state.minScale) { 111 | scaleResult = canvasReader.state.minScale / canvasScale; 112 | } 113 | if (scale * canvasScale >= canvasReader.state.maxScale) { 114 | scaleResult = canvasReader.state.maxScale / canvasScale; 115 | } 116 | return scaleResult; 117 | } 118 | } 119 | 120 | /// Optimized implementation of [CanvasPolicy]. 121 | /// 122 | /// It enabled only pan of the canvas. 123 | /// 124 | /// It uses [onCanvasScaleStart], [onCanvasScaleUpdate], [onCanvasScaleEnd]. 125 | /// Feel free to override other functions from [CanvasPolicy] and add them to [PolicySet]. 126 | mixin CanvasMovePolicy on BasePolicySet implements CanvasControlPolicy { 127 | @override 128 | AnimationController? _animationController; 129 | 130 | @override 131 | Offset _basePosition = const Offset(0, 0); 132 | 133 | @override 134 | Offset _lastFocalPoint = const Offset(0, 0); 135 | 136 | @override 137 | Offset transformPosition = const Offset(0, 0); 138 | @override 139 | double transformScale = 1.0; 140 | 141 | @override 142 | bool canUpdateCanvasModel = false; 143 | 144 | @override 145 | AnimationController? getAnimationController() { 146 | return _animationController; 147 | } 148 | 149 | @override 150 | void setAnimationController(AnimationController animationController) { 151 | _animationController = animationController; 152 | } 153 | 154 | @override 155 | void disposeAnimationController() { 156 | _animationController?.dispose(); 157 | } 158 | 159 | @override 160 | void onCanvasScaleStart(ScaleStartDetails details) { 161 | _basePosition = canvasReader.state.position; 162 | 163 | _lastFocalPoint = details.focalPoint; 164 | } 165 | 166 | @override 167 | void onCanvasScaleUpdate(ScaleUpdateDetails details) { 168 | if (canUpdateCanvasModel) { 169 | _animationController?.repeat(); 170 | _updateCanvasModelWithLastValues(); 171 | 172 | transformPosition += details.focalPoint - _lastFocalPoint; 173 | 174 | _lastFocalPoint = details.focalPoint; 175 | 176 | _animationController?.reset(); 177 | } 178 | } 179 | 180 | @override 181 | void onCanvasScaleEnd(ScaleEndDetails details) { 182 | if (canUpdateCanvasModel) { 183 | _updateCanvasModelWithLastValues(); 184 | } 185 | 186 | _animationController?.reset(); 187 | 188 | transformPosition = const Offset(0, 0); 189 | 190 | canvasWriter.state.updateCanvas(); 191 | } 192 | 193 | @override 194 | void _updateCanvasModelWithLastValues() { 195 | canvasWriter.state.setPosition(_basePosition + transformPosition); 196 | canUpdateCanvasModel = false; 197 | } 198 | 199 | @override 200 | void onCanvasPointerSignal(PointerSignalEvent event) {} 201 | 202 | @override 203 | double keepScaleInBounds(double scale, double canvasScale) { 204 | return 1.0; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/defaults/link_attachment_crystal_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/link_attachment_policy.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Attaches a link endpoint to border of an crystal shape. 6 | mixin LinkAttachmentCrystalPolicy implements LinkAttachmentPolicy { 7 | @override 8 | Alignment getLinkEndpointAlignment( 9 | ComponentData componentData, 10 | Offset targetPoint, 11 | ) { 12 | Offset pointPosition = targetPoint - 13 | (componentData.position + componentData.size.center(Offset.zero)); 14 | pointPosition = Offset( 15 | pointPosition.dx / componentData.size.width, 16 | pointPosition.dy / componentData.size.height, 17 | ); 18 | 19 | Offset pointAlignment = 20 | pointPosition / (pointPosition.dx.abs() + pointPosition.dy.abs()); 21 | 22 | return Alignment(pointAlignment.dx, pointAlignment.dy); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/defaults/link_attachment_oval_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/link_attachment_policy.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Attaches a link endpoint to border of an oval. 6 | mixin LinkAttachmentOvalPolicy implements LinkAttachmentPolicy { 7 | @override 8 | Alignment getLinkEndpointAlignment( 9 | ComponentData componentData, 10 | Offset targetPoint, 11 | ) { 12 | Offset pointPosition = targetPoint - 13 | (componentData.position + componentData.size.center(Offset.zero)); 14 | pointPosition = Offset( 15 | pointPosition.dx / componentData.size.width, 16 | pointPosition.dy / componentData.size.height, 17 | ); 18 | 19 | Offset pointAlignment = pointPosition / pointPosition.distance; 20 | 21 | return Alignment(pointAlignment.dx, pointAlignment.dy); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/defaults/link_attachment_rect_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/link_attachment_policy.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Attaches a link endpoint to border of a rectangle. 6 | mixin LinkAttachmentRectPolicy implements LinkAttachmentPolicy { 7 | @override 8 | Alignment getLinkEndpointAlignment( 9 | ComponentData componentData, 10 | Offset targetPoint, 11 | ) { 12 | Offset pointPosition = targetPoint - 13 | (componentData.position + componentData.size.center(Offset.zero)); 14 | pointPosition = Offset( 15 | pointPosition.dx / componentData.size.width, 16 | pointPosition.dy / componentData.size.height, 17 | ); 18 | 19 | Offset pointAlignment; 20 | if (pointPosition.dx.abs() >= pointPosition.dy.abs()) { 21 | pointAlignment = Offset( 22 | pointPosition.dx / pointPosition.dx.abs(), 23 | pointPosition.dy / pointPosition.dx.abs(), 24 | ); 25 | } else { 26 | pointAlignment = Offset( 27 | pointPosition.dx / pointPosition.dy.abs(), 28 | pointPosition.dy / pointPosition.dy.abs(), 29 | ); 30 | } 31 | return Alignment(pointAlignment.dx, pointAlignment.dy); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/defaults/link_control_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/link_policy.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// Optimized implementation of [LinkPolicy]. 5 | /// 6 | /// Adding new joints and showing joints on link tap. 7 | /// 8 | /// It uses [onLinkTapUp], [onLinkScaleStart], [onLinkScaleUpdate], [onLinkLongPressStart], [onLinkLongPressMoveUpdate]. 9 | /// Feel free to override other functions from [LinkPolicy] and add them to [PolicySet]. 10 | mixin LinkControlPolicy implements LinkPolicy { 11 | @override 12 | void onLinkTapUp(String linkId, TapUpDetails details) { 13 | canvasWriter.model.hideAllLinkJoints(); 14 | canvasWriter.model.showLinkJoints(linkId); 15 | } 16 | 17 | int? _segmentIndex; 18 | 19 | @override 20 | void onLinkScaleStart(String linkId, ScaleStartDetails details) { 21 | canvasWriter.model.hideAllLinkJoints(); 22 | canvasWriter.model.showLinkJoints(linkId); 23 | _segmentIndex = canvasReader.model 24 | .determineLinkSegmentIndex(linkId, details.localFocalPoint); 25 | if (_segmentIndex != null) { 26 | canvasWriter.model.insertLinkMiddlePoint( 27 | linkId, details.localFocalPoint, _segmentIndex!); 28 | canvasWriter.model.updateLink(linkId); 29 | } 30 | } 31 | 32 | @override 33 | void onLinkScaleUpdate(String linkId, ScaleUpdateDetails details) { 34 | if (_segmentIndex != null) { 35 | canvasWriter.model.setLinkMiddlePointPosition( 36 | linkId, details.localFocalPoint, _segmentIndex!); 37 | canvasWriter.model.updateLink(linkId); 38 | } 39 | } 40 | 41 | @override 42 | void onLinkLongPressStart(String linkId, LongPressStartDetails details) { 43 | canvasWriter.model.hideAllLinkJoints(); 44 | canvasWriter.model.showLinkJoints(linkId); 45 | _segmentIndex = canvasReader.model 46 | .determineLinkSegmentIndex(linkId, details.localPosition); 47 | if (_segmentIndex != null) { 48 | canvasWriter.model 49 | .insertLinkMiddlePoint(linkId, details.localPosition, _segmentIndex!); 50 | canvasWriter.model.updateLink(linkId); 51 | } 52 | } 53 | 54 | @override 55 | void onLinkLongPressMoveUpdate( 56 | String linkId, LongPressMoveUpdateDetails details) { 57 | if (_segmentIndex != null) { 58 | canvasWriter.model.setLinkMiddlePointPosition( 59 | linkId, details.localPosition, _segmentIndex!); 60 | canvasWriter.model.updateLink(linkId); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/policy/defaults/link_joint_control_policy.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/link_joints_policy.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// Optimized implementation of [LinkJointPolicy]. 5 | /// 6 | /// Moving and removing link joints. 7 | /// 8 | /// It uses [onLinkJointLongPress], [onLinkJointScaleUpdate]. 9 | /// Feel free to override other functions from [LinkJointPolicy] and add them to [PolicySet]. 10 | mixin LinkJointControlPolicy implements LinkJointPolicy { 11 | @override 12 | void onLinkJointLongPress(int jointIndex, String linkId) { 13 | canvasWriter.model.removeLinkMiddlePoint(linkId, jointIndex); 14 | canvasWriter.model.updateLink(linkId); 15 | } 16 | 17 | @override 18 | void onLinkJointScaleUpdate( 19 | int jointIndex, 20 | String linkId, 21 | ScaleUpdateDetails details, 22 | ) { 23 | canvasWriter.model.setLinkMiddlePointPosition( 24 | linkId, 25 | details.localFocalPoint, 26 | jointIndex, 27 | ); 28 | canvasWriter.model.updateLink(linkId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/rw/canvas_reader.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/rw/model_reader.dart'; 2 | import 'package:diagram_editor/src/abstraction_layer/rw/state_reader.dart'; 3 | 4 | /// Takes care of reading from model and state of the canvas. 5 | class CanvasReader { 6 | /// Access to canvas model (components, links..). 7 | final CanvasModelReader model; 8 | 9 | /// Access to canvas state data (canvas scale, position..). 10 | final CanvasStateReader state; 11 | 12 | CanvasReader(this.model, this.state); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/rw/canvas_writer.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/rw/model_writer.dart'; 2 | import 'package:diagram_editor/src/abstraction_layer/rw/state_writer.dart'; 3 | 4 | /// Takes care of writing to model and state of the canvas. 5 | class CanvasWriter { 6 | /// Access to canvas model (components, links and all the functions to change the model). 7 | final CanvasModelWriter model; 8 | 9 | /// Access to canvas state data (canvas scale, position..). 10 | final CanvasStateWriter state; 11 | 12 | CanvasWriter(this.model, this.state); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/rw/model_reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'dart:convert'; 3 | 4 | import 'package:diagram_editor/src/canvas_context/canvas_model.dart'; 5 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 6 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 7 | import 'package:diagram_editor/src/canvas_context/model/link_data.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class CanvasModelReader { 11 | final CanvasModel canvasModel; 12 | final CanvasState canvasState; 13 | 14 | /// Allows you to read data from the model (component and link data). 15 | CanvasModelReader(this.canvasModel, this.canvasState); 16 | 17 | /// Returns [true] if a component with provided [id] exists. Returns [false] otherwise. 18 | bool componentExist(String id) { 19 | return canvasModel.componentExists(id); 20 | } 21 | 22 | /// Returns a component with [id]. 23 | /// 24 | /// If there is no component with [id] in the model, it returns null. 25 | ComponentData getComponent(String id) { 26 | assert(componentExist(id), 'model does not contain this component id: $id'); 27 | return canvasModel.getComponent(id); 28 | } 29 | 30 | /// Returns all existing components in the model as a [HashMap]. 31 | /// 32 | /// Key of the HashMap element is component's id. 33 | HashMap getAllComponents() { 34 | return canvasModel.getAllComponents(); 35 | } 36 | 37 | /// Returns [true] if a link with provided [id] exists. Returns [false] otherwise. 38 | bool linkExist(String id) { 39 | return canvasModel.linkExists(id); 40 | } 41 | 42 | /// Returns a link with [id]. 43 | /// 44 | /// If there is no link with [id] in the model, it returns null. 45 | LinkData getLink(String id) { 46 | assert(linkExist(id), 'model does not contain this link id: $id'); 47 | return canvasModel.getLink(id); 48 | } 49 | 50 | /// Returns all existing links in the model as a [HashMap]. 51 | /// 52 | /// Key of the HashMap element is link's id. 53 | HashMap getAllLinks() { 54 | return canvasModel.getAllLinks(); 55 | } 56 | 57 | /// If a link is compound from more than one segments this returns an index of the link segment, which was tapped on. 58 | /// 59 | /// Segments are indexed from 1. 60 | /// If there is no link segment on the tap location it returns null. 61 | /// It should take a localPosition from a onLinkTap or similar. 62 | int? determineLinkSegmentIndex( 63 | String linkId, 64 | Offset tapPosition, 65 | ) { 66 | return canvasModel.getLink(linkId).determineLinkSegmentIndex( 67 | tapPosition, 68 | canvasState.position, 69 | canvasState.scale, 70 | ); 71 | } 72 | 73 | /// Returns [String] that contains serialized diagram in JSON format. 74 | /// 75 | /// To serialize dynamic data of components/links [toJson] function must be defined. 76 | String serializeDiagram() { 77 | return jsonEncode(canvasModel.getDiagram()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/rw/model_writer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:diagram_editor/src/canvas_context/canvas_model.dart'; 4 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 5 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 6 | import 'package:diagram_editor/src/canvas_context/model/diagram_data.dart'; 7 | import 'package:diagram_editor/src/utils/link_style.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class ModelWriter { 11 | final CanvasModel _canvasModel; 12 | final CanvasState _canvasState; 13 | 14 | ModelWriter(this._canvasModel, this._canvasState); 15 | } 16 | 17 | class CanvasModelWriter extends ModelWriter 18 | with ComponentWriter, LinkWriter, ConnectionWriter { 19 | /// Allows you to change the model. 20 | CanvasModelWriter(super.canvasModel, super.canvasState); 21 | 22 | /// Adds [componentData] to the canvas model. 23 | /// 24 | /// Returns component's id (if [componentData] doesn't contain id, new id if generated). 25 | /// Canvas is updated and this new components is shown on it. 26 | String addComponent(ComponentData componentData) { 27 | return _canvasModel.addComponent(componentData); 28 | } 29 | 30 | /// Removes a component with [componentId] and all its links. 31 | void removeComponent(String componentId) { 32 | assert(_canvasModel.componentExists(componentId), 33 | 'model does not contain this component id: $componentId'); 34 | removeComponentParent(componentId); 35 | _removeParentFromChildren(componentId); 36 | _canvasModel.removeComponent(componentId); 37 | } 38 | 39 | /// Removes a component with [componentId] and also removes all its children components. 40 | void removeComponentWithChildren(String componentId) { 41 | assert(_canvasModel.componentExists(componentId), 42 | 'model does not contain this component id: $componentId'); 43 | List componentsToRemove = []; 44 | _removeComponentWithChildren(componentId, componentsToRemove); 45 | componentsToRemove.reversed.forEach(removeComponent); 46 | } 47 | 48 | void _removeComponentWithChildren(String componentId, List toRemove) { 49 | toRemove.add(componentId); 50 | _canvasModel.getComponent(componentId).childrenIds.forEach((childId) { 51 | _removeComponentWithChildren(childId, toRemove); 52 | }); 53 | } 54 | 55 | /// Removes all components in the model. All links are also removed with the components. 56 | void removeAllComponents() { 57 | _canvasModel.removeAllComponents(); 58 | } 59 | 60 | /// Removes link with [linkId] from the model. 61 | /// 62 | /// Also deletes the connection information from both components which were connected with this link. 63 | void removeLink(String linkId) { 64 | assert(_canvasModel.linkExists(linkId), 65 | 'model does not contain this link id: $linkId'); 66 | _canvasModel.removeLink(linkId); 67 | } 68 | 69 | /// Removes all links from the model. 70 | void removeAllLinks() { 71 | _canvasModel.removeAllLinks(); 72 | } 73 | 74 | /// Loads a diagram from json string. 75 | /// 76 | /// !!! Beware of passing correct json string. 77 | /// The diagram may become unstable if any data are manipulated. 78 | /// Deleting existing diagram is recommended. 79 | void deserializeDiagram( 80 | String json, { 81 | Function(Map json)? decodeCustomComponentData, 82 | Function(Map json)? decodeCustomLinkData, 83 | }) { 84 | final diagram = DiagramData.fromJson( 85 | jsonDecode(json), 86 | decodeCustomComponentData: decodeCustomComponentData, 87 | decodeCustomLinkData: decodeCustomLinkData, 88 | ); 89 | for (final componentData in diagram.components) { 90 | _canvasModel.components[componentData.id] = componentData; 91 | } 92 | for (final linkData in diagram.links) { 93 | _canvasModel.links[linkData.id] = linkData; 94 | linkData.updateLink(); 95 | } 96 | _canvasModel.updateCanvas(); 97 | } 98 | } 99 | 100 | mixin ComponentWriter on ModelWriter { 101 | /// Update a component with [componentId]. 102 | /// 103 | /// It calls [notifyListeners] function of [ChangeNotifier] on [ComponentData]. 104 | void updateComponent(String? componentId) { 105 | if (componentId == null) return; 106 | assert(_canvasModel.componentExists(componentId), 107 | 'model does not contain this component id: $componentId'); 108 | _canvasModel.getComponent(componentId).updateComponent(); 109 | } 110 | 111 | /// Sets the position of the component to [position] value. 112 | void setComponentPosition(String componentId, Offset position) { 113 | assert(_canvasModel.componentExists(componentId), 114 | 'model does not contain this component id: $componentId'); 115 | _canvasModel.getComponent(componentId).setPosition(position); 116 | _canvasModel.updateLinks(componentId); 117 | } 118 | 119 | /// Translates the component by [offset] value. 120 | void moveComponent(String componentId, Offset offset) { 121 | assert(_canvasModel.componentExists(componentId), 122 | 'model does not contain this component id: $componentId'); 123 | _canvasModel.getComponent(componentId).move(offset / _canvasState.scale); 124 | _canvasModel.updateLinks(componentId); 125 | } 126 | 127 | /// Translates the component by [offset] value and all its children as well. 128 | void moveComponentWithChildren(String componentId, Offset offset) { 129 | assert(_canvasModel.componentExists(componentId), 130 | 'model does not contain this component id: $componentId'); 131 | moveComponent(componentId, offset); 132 | _canvasModel.getComponent(componentId).childrenIds.forEach((childId) { 133 | moveComponentWithChildren(childId, offset); 134 | }); 135 | } 136 | 137 | /// Removes all connections that the component with [componentId] has. 138 | void removeComponentConnections(String componentId) { 139 | assert(_canvasModel.componentExists(componentId), 140 | 'model does not contain this component id: $componentId'); 141 | _canvasModel.removeComponentConnections(componentId); 142 | } 143 | 144 | /// Updates all links (their position) connected to the component with [componentId]. 145 | /// 146 | /// Use it when the component is somehow changed (its size or position) and the links are not updated to their proper positions. 147 | void updateComponentLinks(String componentId) { 148 | assert(_canvasModel.componentExists(componentId), 149 | 'model does not contain this component id: $componentId'); 150 | _canvasModel.updateLinks(componentId); 151 | } 152 | 153 | /// Sets the component's z-order to [zOrder]. 154 | /// 155 | /// Higher z-order means that the component will be shown on top of another component with lower z-order. 156 | void setComponentZOrder(String componentId, int zOrder) { 157 | assert(_canvasModel.componentExists(componentId), 158 | 'model does not contain this component id: $componentId'); 159 | _canvasModel.setComponentZOrder(componentId, zOrder); 160 | } 161 | 162 | /// Sets the components's z-order to the highest z-order value of all components +1. 163 | int moveComponentToTheFront(String componentId) { 164 | assert(_canvasModel.componentExists(componentId), 165 | 'model does not contain this component id: $componentId'); 166 | return _canvasModel.moveComponentToTheFront(componentId); 167 | } 168 | 169 | /// Sets the components's z-order to the highest z-order value of all components +1 and sets z-order of its children to +2... 170 | int moveComponentToTheFrontWithChildren(String componentId) { 171 | assert(_canvasModel.componentExists(componentId), 172 | 'model does not contain this component id: $componentId'); 173 | int zOrder = moveComponentToTheFront(componentId); 174 | _setZOrderToChildren(componentId, zOrder); 175 | return zOrder; 176 | } 177 | 178 | void _setZOrderToChildren(String componentId, int zOrder) { 179 | assert(_canvasModel.componentExists(componentId), 180 | 'model does not contain this component id: $componentId'); 181 | setComponentZOrder(componentId, zOrder); 182 | _canvasModel.getComponent(componentId).childrenIds.forEach((childId) { 183 | _setZOrderToChildren(childId, zOrder + 1); 184 | }); 185 | } 186 | 187 | /// Sets the components's z-order to the lowest z-order value of all components -1. 188 | int moveComponentToTheBack(String componentId) { 189 | assert(_canvasModel.componentExists(componentId), 190 | 'model does not contain this component id: $componentId'); 191 | return _canvasModel.moveComponentToTheBack(componentId); 192 | } 193 | 194 | /// Sets the components's z-order to the lowest z-order value of all components -1 and sets z-order of its children to one more than the component and their children to one more.. 195 | int moveComponentToTheBackWithChildren(String componentId) { 196 | assert(_canvasModel.componentExists(componentId), 197 | 'model does not contain this component id: $componentId'); 198 | int zOrder = moveComponentToTheBack(componentId); 199 | _setZOrderToChildren(componentId, zOrder); 200 | return zOrder; 201 | } 202 | 203 | /// Changes the component's size by [deltaSize]. 204 | /// 205 | /// You cannot change its size to smaller than [minSize] defined on the component. 206 | void resizeComponent(String componentId, Offset deltaSize) { 207 | assert(_canvasModel.componentExists(componentId), 208 | 'model does not contain this component id: $componentId'); 209 | _canvasModel.getComponent(componentId).resizeDelta(deltaSize); 210 | } 211 | 212 | /// Sets the component's to [size]. 213 | void setComponentSize(String componentId, Size size) { 214 | assert(_canvasModel.componentExists(componentId), 215 | 'model does not contain this component id: $componentId'); 216 | _canvasModel.getComponent(componentId).setSize(size); 217 | } 218 | 219 | /// Sets the component's parent. 220 | /// 221 | /// It's not possible to make a parent-child loop. (its ancestor cannot be its child) 222 | void setComponentParent(String componentId, String parentId) { 223 | assert(_canvasModel.componentExists(componentId), 224 | 'model does not contain this component id: $componentId'); 225 | removeComponentParent(componentId); 226 | if (_checkParentChildLoop(componentId, parentId)) { 227 | _canvasModel.getComponent(componentId).setParent(parentId); 228 | _canvasModel.getComponent(parentId).addChild(componentId); 229 | } 230 | } 231 | 232 | bool _checkParentChildLoop(String componentId, String parentId) { 233 | if (componentId == parentId) return false; 234 | final parentIdOfParent = _canvasModel.getComponent(parentId).parentId; 235 | if (parentIdOfParent != null) { 236 | return _checkParentChildLoop(componentId, parentIdOfParent); 237 | } 238 | 239 | return true; 240 | } 241 | 242 | /// Removes the component's parent from a component. 243 | /// 244 | /// It also removes child from former parent. 245 | void removeComponentParent(String componentId) { 246 | assert(_canvasModel.componentExists(componentId), 247 | 'model does not contain this component id: $componentId'); 248 | final parentId = _canvasModel.getComponent(componentId).parentId; 249 | if (parentId != null) { 250 | _canvasModel.getComponent(componentId).removeParent(); 251 | _canvasModel.getComponent(parentId).removeChild(componentId); 252 | } 253 | } 254 | 255 | void _removeParentFromChildren(componentId) { 256 | assert(_canvasModel.componentExists(componentId), 257 | 'model does not contain this component id: $componentId'); 258 | final component = _canvasModel.getComponent(componentId); 259 | final childrenToRemove = List.from(component.childrenIds); 260 | for (final childId in childrenToRemove) { 261 | removeComponentParent(childId); 262 | } 263 | } 264 | } 265 | 266 | mixin LinkWriter on ModelWriter { 267 | /// Makes all link's joints visible. 268 | void showLinkJoints(String linkId) { 269 | assert(_canvasModel.linkExists(linkId), 270 | 'model does not contain this link id: $linkId'); 271 | _canvasModel.getLink(linkId).showJoints(); 272 | } 273 | 274 | /// Makes all link's joints invisible. 275 | void hideLinkJoints(String linkId) { 276 | assert(_canvasModel.linkExists(linkId), 277 | 'model does not contain this link id: $linkId'); 278 | _canvasModel.getLink(linkId).hideJoints(); 279 | } 280 | 281 | /// Makes invisible all link joints on the canvas. 282 | void hideAllLinkJoints() { 283 | for (final link in _canvasModel.links.values) { 284 | link.hideJoints(); 285 | } 286 | } 287 | 288 | /// Updates the link. 289 | /// 290 | /// Use it when something is changed and the link is not updated to its proper positions. 291 | void updateLink(String linkId) { 292 | assert(_canvasModel.linkExists(linkId), 293 | 'model does not contain this link id: $linkId'); 294 | _canvasModel.updateLinks(_canvasModel.getLink(linkId).sourceComponentId); 295 | _canvasModel.updateLinks(_canvasModel.getLink(linkId).targetComponentId); 296 | } 297 | 298 | /// Creates a new link's joint on [point] location. 299 | /// 300 | /// [index] is an index of link's segment where you want to insert the point. 301 | /// Indexed from 1. 302 | /// When the link is a straight line you want to add a point to index 1. 303 | void insertLinkMiddlePoint(String linkId, Offset point, int index) { 304 | assert(_canvasModel.linkExists(linkId), 305 | 'model does not contain this link id: $linkId'); 306 | _canvasModel 307 | .getLink(linkId) 308 | .insertMiddlePoint(_canvasState.fromCanvasCoordinates(point), index); 309 | } 310 | 311 | /// Sets the new position ([point]) to the existing link's joint point. 312 | /// 313 | /// Joints are indexed from 1. 314 | void setLinkMiddlePointPosition(String linkId, Offset point, int index) { 315 | assert(_canvasModel.linkExists(linkId), 316 | 'model does not contain this link id: $linkId'); 317 | _canvasModel.getLink(linkId).setMiddlePointPosition( 318 | _canvasState.fromCanvasCoordinates(point), index); 319 | } 320 | 321 | /// Updates link's joint position by [offset]. 322 | /// 323 | /// Joints are indexed from 1. 324 | void moveLinkMiddlePoint(String linkId, Offset offset, int index) { 325 | assert(_canvasModel.linkExists(linkId), 326 | 'model does not contain this link id: $linkId'); 327 | _canvasModel 328 | .getLink(linkId) 329 | .moveMiddlePoint(offset / _canvasState.scale, index); 330 | } 331 | 332 | /// Removes the joint on [index]th place from the link. 333 | /// 334 | /// Joints are indexed from 1. 335 | void removeLinkMiddlePoint(String linkId, int index) { 336 | assert(_canvasModel.linkExists(linkId), 337 | 'model does not contain this link id: $linkId'); 338 | _canvasModel.getLink(linkId).removeMiddlePoint(index); 339 | } 340 | 341 | /// Updates all link's joints position by [offset]. 342 | void moveAllLinkMiddlePoints(String linkId, Offset position) { 343 | assert(_canvasModel.linkExists(linkId), 344 | 'model does not contain this link id: $linkId'); 345 | _canvasModel 346 | .getLink(linkId) 347 | .moveAllMiddlePoints(position / _canvasState.scale); 348 | } 349 | } 350 | 351 | mixin ConnectionWriter on ModelWriter { 352 | /// Connects two components with a new link. The link is added to the model. 353 | /// 354 | /// The link points from [sourceComponentId] to [targetComponentId]. 355 | /// Connection information is added to both components. 356 | /// 357 | /// Returns id of the created link. 358 | /// 359 | /// You can define the design of the link with [LinkStyle]. 360 | /// You can add your own dynamic [data] to the link. 361 | String connectTwoComponents({ 362 | required String sourceComponentId, 363 | required String targetComponentId, 364 | LinkStyle? linkStyle, 365 | dynamic data, 366 | }) { 367 | assert(_canvasModel.componentExists(sourceComponentId)); 368 | assert(_canvasModel.componentExists(targetComponentId)); 369 | return _canvasModel.connectTwoComponents( 370 | sourceComponentId, 371 | targetComponentId, 372 | linkStyle, 373 | data, 374 | ); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/rw/state_reader.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CanvasStateReader { 5 | final CanvasState canvasState; 6 | 7 | /// Allows you to read state (position and scale) of the canvas. 8 | CanvasStateReader(this.canvasState); 9 | 10 | /// Position of the canvas. Coordinates where the (0, 0) of the canvas is currently located. 11 | /// 12 | /// Initial value equals to [Offset(0, 0)]. 13 | Offset get position => canvasState.position; 14 | 15 | /// Scale od the canvas. It must be always positive. 16 | /// 17 | /// Initial value equals to 1. 18 | double get scale => canvasState.scale; 19 | 20 | /// Determine how fast the canvas is scale when user uses mouse's wheel. 21 | double get mouseScaleSpeed => canvasState.mouseScaleSpeed; 22 | 23 | /// Maximal scale of the canvas. User cannot zoom the canvas more than this value. 24 | double get maxScale => canvasState.maxScale; 25 | 26 | /// Minimal scale of the canvas. User cannot zoom the canvas less than this value. 27 | double get minScale => canvasState.minScale; 28 | 29 | /// A base color of the canvas. 30 | Color get color => canvasState.color; 31 | 32 | /// Calculates position from Canvas to use it in the model. 33 | /// 34 | /// Use when you have localPosition or localOffset from widget on canvas to get real (translated and scaled) coordinates on canvas. 35 | Offset fromCanvasCoordinates(Offset position) { 36 | return canvasState.fromCanvasCoordinates(position); 37 | } 38 | 39 | /// Calculates position from the model to use it on Canvas. 40 | /// 41 | /// Use when you want to set widget's position on scaled or translated canvas, eg. in Positioned widget (top, left). 42 | /// Usually in [ComponentWidgetsPolicy] or [LinkWidgetsPolicy]. 43 | Offset toCanvasCoordinates(Offset position) { 44 | return canvasState.toCanvasCoordinates(position); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/abstraction_layer/rw/state_writer.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CanvasStateWriter { 5 | final CanvasState _canvasState; 6 | 7 | /// Allows you to change the state of the canvas. 8 | CanvasStateWriter(this._canvasState); 9 | 10 | /// Updates everything on canvas. 11 | /// 12 | /// It should be used as little as possible, it demands a lot of performance. 13 | /// Use only in case that something on canvas is not updated. 14 | /// It calls [notifyListeners] function of [ChangeNotifier]. 15 | void updateCanvas() { 16 | _canvasState.updateCanvas(); 17 | } 18 | 19 | /// Sets the position of the canvas to [position] value. 20 | void setPosition(Offset position) { 21 | _canvasState.setPosition(position); 22 | } 23 | 24 | /// Sets the scale of the canvas to [scale] value. 25 | /// 26 | /// Scale value should be positive. 27 | void setScale(double scale) { 28 | assert(scale > 0); 29 | _canvasState.setScale(scale); 30 | } 31 | 32 | /// Translates the canvas by [offset]. 33 | void updatePosition(Offset offset) { 34 | _canvasState.updatePosition(offset); 35 | } 36 | 37 | /// Multiplies the scale of the canvas by [scale]. 38 | void updateScale(double scale) { 39 | _canvasState.updateScale(scale); 40 | } 41 | 42 | /// Sets the position of the canvas to (0, 0) and scale to 1. 43 | void resetCanvasView() { 44 | _canvasState.resetCanvasView(); 45 | } 46 | 47 | /// Sets the base color of the canvas. 48 | void setCanvasColor(Color color) { 49 | _canvasState.color = color; 50 | } 51 | 52 | /// Sets the maximal possible scale of the canvas. 53 | void setMaxScale(double scale) { 54 | _canvasState.maxScale = scale; 55 | } 56 | 57 | /// Sets the minimal possible scale of the canvas. 58 | void setMinScale(double scale) { 59 | _canvasState.minScale = scale; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/canvas_context/canvas_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 4 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 5 | import 'package:diagram_editor/src/canvas_context/model/connection.dart'; 6 | import 'package:diagram_editor/src/canvas_context/model/diagram_data.dart'; 7 | import 'package:diagram_editor/src/canvas_context/model/link_data.dart'; 8 | import 'package:diagram_editor/src/utils/link_style.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:uuid/uuid.dart'; 11 | 12 | class CanvasModel with ChangeNotifier { 13 | final Uuid _uuid = const Uuid(); 14 | HashMap components = HashMap(); 15 | HashMap links = HashMap(); 16 | PolicySet policySet; 17 | 18 | CanvasModel(this.policySet); 19 | 20 | DiagramData getDiagram() { 21 | return DiagramData( 22 | components: components.values.toList(), 23 | links: links.values.toList(), 24 | ); 25 | } 26 | 27 | void updateCanvas() { 28 | notifyListeners(); 29 | } 30 | 31 | bool componentExists(String id) { 32 | return components.containsKey(id); 33 | } 34 | 35 | ComponentData getComponent(String id) { 36 | return components[id]!; 37 | } 38 | 39 | HashMap getAllComponents() { 40 | return components; 41 | } 42 | 43 | bool linkExists(String id) { 44 | return links.containsKey(id); 45 | } 46 | 47 | LinkData getLink(String id) { 48 | return links[id]!; 49 | } 50 | 51 | HashMap getAllLinks() { 52 | return links; 53 | } 54 | 55 | /// Returns componentData id. useful when the id is set automatically. 56 | String addComponent(ComponentData componentData) { 57 | components[componentData.id] = componentData; 58 | notifyListeners(); 59 | return componentData.id; 60 | } 61 | 62 | void removeComponent(String id) { 63 | removeComponentConnections(id); 64 | components.remove(id); 65 | notifyListeners(); 66 | } 67 | 68 | void removeComponentConnections(String id) { 69 | assert(components.keys.contains(id)); 70 | 71 | List linksToRemove = []; 72 | 73 | getComponent(id).connections.forEach((connection) { 74 | linksToRemove.add(connection.connectionId); 75 | }); 76 | 77 | linksToRemove.forEach(removeLink); 78 | notifyListeners(); 79 | } 80 | 81 | void removeAllComponents() { 82 | links.clear(); 83 | components.clear(); 84 | notifyListeners(); 85 | } 86 | 87 | void setComponentZOrder(String componentId, int zOrder) { 88 | getComponent(componentId).zOrder = zOrder; 89 | notifyListeners(); 90 | } 91 | 92 | /// You cannot use is during any movement, because the order will change so the moving item will change. 93 | /// Returns new zOrder 94 | int moveComponentToTheFront(String componentId) { 95 | int zOrderMax = getComponent(componentId).zOrder; 96 | for (final component in components.values) { 97 | if (component.zOrder > zOrderMax) { 98 | zOrderMax = component.zOrder; 99 | } 100 | } 101 | getComponent(componentId).zOrder = zOrderMax + 1; 102 | notifyListeners(); 103 | return zOrderMax + 1; 104 | } 105 | 106 | /// You cannot use is during any movement, because the order will change so the moving item will change. 107 | /// /// Returns new zOrder 108 | int moveComponentToTheBack(String componentId) { 109 | int zOrderMin = getComponent(componentId).zOrder; 110 | for (final component in components.values) { 111 | if (component.zOrder < zOrderMin) { 112 | zOrderMin = component.zOrder; 113 | } 114 | } 115 | getComponent(componentId).zOrder = zOrderMin - 1; 116 | notifyListeners(); 117 | return zOrderMin - 1; 118 | } 119 | 120 | void addLink(LinkData linkData) { 121 | links[linkData.id] = linkData; 122 | notifyListeners(); 123 | } 124 | 125 | void removeLink(String linkId) { 126 | getComponent(getLink(linkId).sourceComponentId).removeConnection(linkId); 127 | getComponent(getLink(linkId).targetComponentId).removeConnection(linkId); 128 | links.remove(linkId); 129 | notifyListeners(); 130 | } 131 | 132 | void removeAllLinks() { 133 | for (final component in components.values) { 134 | removeComponentConnections(component.id); 135 | } 136 | } 137 | 138 | /// Creates a link between components. Returns created link's id. 139 | String connectTwoComponents( 140 | String sourceComponentId, 141 | String targetComponentId, 142 | LinkStyle? linkStyle, 143 | dynamic data, 144 | ) { 145 | var linkId = _uuid.v4(); 146 | var sourceComponent = getComponent(sourceComponentId); 147 | var targetComponent = getComponent(targetComponentId); 148 | 149 | sourceComponent.addConnection( 150 | ConnectionOut( 151 | connectionId: linkId, 152 | otherComponentId: targetComponentId, 153 | ), 154 | ); 155 | targetComponent.addConnection( 156 | ConnectionIn( 157 | connectionId: linkId, 158 | otherComponentId: sourceComponentId, 159 | ), 160 | ); 161 | 162 | var sourceLinkAlignment = policySet.getLinkEndpointAlignment( 163 | sourceComponent, 164 | targetComponent.position + targetComponent.size.center(Offset.zero), 165 | ); 166 | var targetLinkAlignment = policySet.getLinkEndpointAlignment( 167 | targetComponent, 168 | sourceComponent.position + sourceComponent.size.center(Offset.zero), 169 | ); 170 | 171 | links[linkId] = LinkData( 172 | id: linkId, 173 | sourceComponentId: sourceComponentId, 174 | targetComponentId: targetComponentId, 175 | linkPoints: [ 176 | sourceComponent.position + 177 | sourceComponent.getPointOnComponent(sourceLinkAlignment), 178 | targetComponent.position + 179 | targetComponent.getPointOnComponent(targetLinkAlignment), 180 | ], 181 | linkStyle: linkStyle ?? LinkStyle(), 182 | data: data, 183 | ); 184 | 185 | notifyListeners(); 186 | return linkId; 187 | } 188 | 189 | void updateLinks(String componentId) { 190 | assert(componentExists(componentId), 191 | 'model does not contain this component id: $componentId'); 192 | var component = getComponent(componentId); 193 | for (final connection in component.connections) { 194 | var link = getLink(connection.connectionId); 195 | 196 | ComponentData sourceComponent = component; 197 | var targetComponent = getComponent(connection.otherComponentId); 198 | 199 | if (connection is ConnectionOut) { 200 | sourceComponent = component; 201 | targetComponent = getComponent(connection.otherComponentId); 202 | } else if (connection is ConnectionIn) { 203 | sourceComponent = getComponent(connection.otherComponentId); 204 | targetComponent = component; 205 | } else { 206 | throw ArgumentError('Invalid port connection.'); 207 | } 208 | 209 | Alignment firstLinkAlignment = 210 | _getLinkEndpointAlignment(sourceComponent, targetComponent, link, 1); 211 | Alignment secondLinkAlignment = _getLinkEndpointAlignment( 212 | targetComponent, sourceComponent, link, link.linkPoints.length - 2); 213 | 214 | _setLinkEndpoints(link, sourceComponent, targetComponent, 215 | firstLinkAlignment, secondLinkAlignment); 216 | } 217 | } 218 | 219 | Alignment _getLinkEndpointAlignment( 220 | ComponentData component1, 221 | ComponentData component2, 222 | LinkData link, 223 | int linkPointIndex, 224 | ) { 225 | if (link.linkPoints.length <= 2) { 226 | return policySet.getLinkEndpointAlignment( 227 | component1, 228 | component2.position + component2.size.center(Offset.zero), 229 | ); 230 | } else { 231 | return policySet.getLinkEndpointAlignment( 232 | component1, 233 | link.linkPoints[linkPointIndex], 234 | ); 235 | } 236 | } 237 | 238 | void _setLinkEndpoints( 239 | LinkData link, 240 | ComponentData component1, 241 | ComponentData component2, 242 | Alignment alignment1, 243 | Alignment alignment2, 244 | ) { 245 | link.setEndpoints( 246 | component1.position + component1.getPointOnComponent(alignment1), 247 | component2.position + component2.getPointOnComponent(alignment2), 248 | ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /lib/src/canvas_context/canvas_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CanvasState with ChangeNotifier { 4 | Offset _position = const Offset(0, 0); 5 | double _scale = 1.0; 6 | 7 | double mouseScaleSpeed = 0.8; 8 | 9 | double maxScale = 8.0; 10 | double minScale = 0.1; 11 | 12 | Color color = Colors.white; 13 | 14 | GlobalKey canvasGlobalKey = GlobalKey(); 15 | 16 | bool shouldAbsorbPointer = false; 17 | 18 | bool isInitialized = false; 19 | 20 | Offset get position => _position; 21 | 22 | double get scale => _scale; 23 | 24 | void updateCanvas() { 25 | notifyListeners(); 26 | } 27 | 28 | void setPosition(Offset position) { 29 | _position = position; 30 | } 31 | 32 | void setScale(double scale) { 33 | _scale = scale; 34 | } 35 | 36 | void updatePosition(Offset offset) { 37 | _position += offset; 38 | } 39 | 40 | void updateScale(double scale) { 41 | _scale *= scale; 42 | } 43 | 44 | void resetCanvasView() { 45 | _position = const Offset(0, 0); 46 | _scale = 1.0; 47 | notifyListeners(); 48 | } 49 | 50 | Offset fromCanvasCoordinates(Offset position) { 51 | return (position - this.position) / scale; 52 | } 53 | 54 | Offset toCanvasCoordinates(Offset position) { 55 | return position * scale + this.position; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/canvas_context/diagram_editor_context.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/abstraction_layer/rw/canvas_reader.dart'; 3 | import 'package:diagram_editor/src/abstraction_layer/rw/canvas_writer.dart'; 4 | import 'package:diagram_editor/src/abstraction_layer/rw/model_reader.dart'; 5 | import 'package:diagram_editor/src/abstraction_layer/rw/model_writer.dart'; 6 | import 'package:diagram_editor/src/abstraction_layer/rw/state_reader.dart'; 7 | import 'package:diagram_editor/src/abstraction_layer/rw/state_writer.dart'; 8 | import 'package:diagram_editor/src/canvas_context/canvas_model.dart'; 9 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 10 | 11 | class DiagramEditorContext { 12 | final CanvasModel _canvasModel; 13 | final CanvasState _canvasState; 14 | 15 | /// Set of policies where all the diagram customization is defined. 16 | final PolicySet policySet; 17 | 18 | /// Canvas model containing all components and links with all the functions. 19 | CanvasModel get canvasModel => _canvasModel; 20 | 21 | /// Canvas state containing for example canvas position and scale. 22 | CanvasState get canvasState => _canvasState; 23 | 24 | /// [DiagramEditorContext] is taken as parameter by [DiagramEditor] widget. 25 | /// 26 | /// Its not generated automatically because you want to use it to copy model or state to another [DiagramEditor]. 27 | DiagramEditorContext({ 28 | required this.policySet, 29 | }) : _canvasModel = CanvasModel(policySet), 30 | _canvasState = CanvasState() { 31 | policySet.initializePolicy(_getReader(), _getWriter()); 32 | } 33 | 34 | /// Allows you to create [DiagramEditorContext] with shared model from another [DiagramEditorContext]. 35 | /// 36 | /// Warning: [LinkAttachmentPolicy] is used in CanvasModel, so this policy will be shared as well, even if you put new one to [PolicySet]. 37 | DiagramEditorContext.withSharedModel( 38 | DiagramEditorContext oldContext, { 39 | required this.policySet, 40 | }) : _canvasModel = oldContext.canvasModel, 41 | _canvasState = CanvasState() { 42 | policySet.initializePolicy(_getReader(), _getWriter()); 43 | } 44 | 45 | /// Allows you to create [DiagramEditorContext] with shared state (eg. canvas position and scale) from another [DiagramEditorContext]. 46 | DiagramEditorContext.withSharedState( 47 | DiagramEditorContext oldContext, { 48 | required this.policySet, 49 | }) : _canvasModel = CanvasModel(policySet), 50 | _canvasState = oldContext.canvasState { 51 | policySet.initializePolicy(_getReader(), _getWriter()); 52 | } 53 | 54 | /// Allows you to create [DiagramEditorContext] with shared model and state from another [DiagramEditorContext]. 55 | /// 56 | /// Warning: [LinkAttachmentPolicy] is used in CanvasModel, so this policy will be shared as well, even if you put new one to [PolicySet]. 57 | DiagramEditorContext.withSharedModelAndState( 58 | DiagramEditorContext oldContext, { 59 | required this.policySet, 60 | }) : _canvasModel = oldContext.canvasModel, 61 | _canvasState = oldContext.canvasState { 62 | policySet.initializePolicy(_getReader(), _getWriter()); 63 | } 64 | 65 | CanvasReader _getReader() { 66 | return CanvasReader( 67 | CanvasModelReader(canvasModel, canvasState), 68 | CanvasStateReader(canvasState), 69 | ); 70 | } 71 | 72 | CanvasWriter _getWriter() { 73 | return CanvasWriter( 74 | CanvasModelWriter(canvasModel, canvasState), 75 | CanvasStateWriter(canvasState), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/canvas_context/model/component_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/canvas_context/model/connection.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:uuid/uuid.dart'; 4 | 5 | class ComponentData with ChangeNotifier { 6 | /// Unique id of this component. 7 | final String id; 8 | 9 | /// Position on the canvas. 10 | Offset position; 11 | 12 | /// Size of the component. 13 | Size size; 14 | 15 | /// Minimal size of a component. 16 | /// 17 | /// When [resizeDelta] is called the size will not go under this value. 18 | final Size minSize; 19 | 20 | /// Component type to distinguish components. 21 | /// 22 | /// You can use it for example to distinguish what [data] type this component has. 23 | final String? type; 24 | 25 | /// This value determines if this component will be above or under other components. 26 | /// Higher value means on the top. 27 | int zOrder = 0; 28 | 29 | /// Assigned parent to this component. 30 | /// 31 | /// Use for hierarchical components. 32 | /// Functions such as [moveComponentWithChildren] work with this property. 33 | String? parentId; 34 | 35 | /// List of children of this component. 36 | /// 37 | /// Use for hierarchical components. 38 | /// Functions such as [moveComponentWithChildren] work with this property. 39 | final List childrenIds = []; 40 | 41 | /// Defines to which components is this components connected and what is the [connectionId]. 42 | /// 43 | /// The connection can be [ConnectionOut] for link going from this component 44 | /// or [ConnectionIn] for link going from another to this component. 45 | final List connections = []; 46 | 47 | /// Dynamic data for you to define your own data for this component. 48 | final dynamic data; 49 | 50 | /// Represents data of a component in the model. 51 | ComponentData({ 52 | String? id, 53 | this.position = Offset.zero, 54 | this.size = const Size(80, 80), 55 | this.minSize = const Size(4, 4), 56 | this.type, 57 | this.data, 58 | }) : assert(minSize <= size), 59 | id = id ?? const Uuid().v4(); 60 | 61 | /// Updates this component on the canvas. 62 | /// 63 | /// Use this function if you somehow changed the component data and you want to propagate the change to canvas. 64 | /// Usually this is already called in most functions such as [move] or [setSize] so it's not necessary to call it again. 65 | /// 66 | /// It calls [notifyListeners] function of [ChangeNotifier]. 67 | void updateComponent() { 68 | notifyListeners(); 69 | } 70 | 71 | /// Translates the component by [offset] value. 72 | void move(Offset offset) { 73 | position += offset; 74 | notifyListeners(); 75 | } 76 | 77 | /// Sets the position of the component to [position] value. 78 | void setPosition(Offset position) { 79 | this.position = position; 80 | notifyListeners(); 81 | } 82 | 83 | /// Adds new connection to this component. 84 | /// 85 | /// Do not use it if you are not sure what you do. This is called in [connectTwoComponents] function. 86 | void addConnection(Connection connection) { 87 | connections.add(connection); 88 | } 89 | 90 | /// Removes existing connection. 91 | /// 92 | /// Do not use it if you are not sure what you do. This is called eg. in [removeLink] function. 93 | void removeConnection(String connectionId) { 94 | connections.removeWhere((conn) => conn.connectionId == connectionId); 95 | } 96 | 97 | /// Changes the component's size by [deltaSize]. 98 | /// 99 | /// You cannot change its size to smaller than [minSize] defined on the component. 100 | void resizeDelta(Offset deltaSize) { 101 | var tempSize = size + deltaSize; 102 | if (tempSize.width < minSize.width) { 103 | tempSize = Size(minSize.width, tempSize.height); 104 | } 105 | if (tempSize.height < minSize.height) { 106 | tempSize = Size(tempSize.width, minSize.height); 107 | } 108 | size = tempSize; 109 | notifyListeners(); 110 | } 111 | 112 | /// Sets the component's to [size]. 113 | void setSize(Size size) { 114 | this.size = size; 115 | notifyListeners(); 116 | } 117 | 118 | /// Returns Offset position on this component from [alignment]. 119 | /// 120 | /// [Alignment.topLeft] returns [Offset.zero] 121 | /// 122 | /// [Alignment.center] or [Alignment(0, 0)] returns the center coordinates on this component. 123 | /// 124 | /// [Alignment.bottomRight] returns offset that is equal to size of this component. 125 | Offset getPointOnComponent(Alignment alignment) { 126 | return Offset( 127 | size.width * ((alignment.x + 1) / 2), 128 | size.height * ((alignment.y + 1) / 2), 129 | ); 130 | } 131 | 132 | /// Sets the component's parent. 133 | /// 134 | /// It's not possible to make a parent-child loop. (its ancestor cannot be its child) 135 | /// 136 | /// You should use it only with [addChild] on the parent's component. 137 | void setParent(String parentId) { 138 | this.parentId = parentId; 139 | } 140 | 141 | /// Removes parent's id from this component data. 142 | /// 143 | /// You should use it only with [removeChild] on the parent's component. 144 | void removeParent() { 145 | parentId = null; 146 | } 147 | 148 | /// Sets the component's parent. 149 | /// 150 | /// It's not possible to make a parent-child loop. (its ancestor cannot be its child) 151 | /// 152 | /// You should use it only with [setParent] on the child's component. 153 | void addChild(String childId) { 154 | childrenIds.add(childId); 155 | } 156 | 157 | /// Removes child's id from children. 158 | /// 159 | /// You should use it only with [removeParent] on the child's component. 160 | void removeChild(String childId) { 161 | childrenIds.remove(childId); 162 | } 163 | 164 | @override 165 | String toString() { 166 | return 'Component data ($id), position: $position'; 167 | } 168 | 169 | ComponentData.fromJson( 170 | Map json, { 171 | Function(Map json)? decodeCustomComponentData, 172 | }) : id = json['id'], 173 | position = Offset(json['position'][0], json['position'][1]), 174 | size = Size(json['size'][0], json['size'][1]), 175 | minSize = Size(json['min_size'][0], json['min_size'][1]), 176 | type = json['type'], 177 | zOrder = json['z_order'], 178 | parentId = json['parent_id'], 179 | data = decodeCustomComponentData?.call(json['dynamic_data']) { 180 | childrenIds.addAll( 181 | (json['children_ids'] as List).map((id) => id as String).toList(), 182 | ); 183 | connections.addAll( 184 | (json['connections'] as List).map((connectionJson) { 185 | return Connection.fromJson(connectionJson); 186 | }), 187 | ); 188 | } 189 | 190 | Map toJson() => { 191 | 'id': id, 192 | 'position': [position.dx, position.dy], 193 | 'size': [size.width, size.height], 194 | 'min_size': [minSize.width, minSize.height], 195 | 'type': type, 196 | 'z_order': zOrder, 197 | 'parent_id': parentId, 198 | 'children_ids': childrenIds, 199 | 'connections': connections, 200 | 'dynamic_data': data?.toJson(), 201 | }; 202 | } 203 | -------------------------------------------------------------------------------- /lib/src/canvas_context/model/connection.dart: -------------------------------------------------------------------------------- 1 | abstract class Connection { 2 | /// Id of this connection. It corresponds to link id. 3 | final String connectionId; 4 | 5 | /// Id of a component to which is the component connected. 6 | final String otherComponentId; 7 | 8 | /// Abstract class that represents connection of a component. 9 | Connection({ 10 | required this.connectionId, 11 | required this.otherComponentId, 12 | }); 13 | 14 | bool contains(String id) { 15 | return id == connectionId; 16 | } 17 | 18 | factory Connection.fromJson(Map json) => (json['type'] == 0) 19 | ? ConnectionOut( 20 | connectionId: json['connection_id'], 21 | otherComponentId: json['other_component_id'], 22 | ) 23 | : ConnectionIn( 24 | connectionId: json['connection_id'], 25 | otherComponentId: json['other_component_id'], 26 | ); 27 | 28 | Map toJson() => (this is ConnectionOut) 29 | ? { 30 | 'type': 0, 31 | 'connection_id': connectionId, 32 | 'other_component_id': otherComponentId, 33 | } 34 | : { 35 | 'type': 1, 36 | 'connection_id': connectionId, 37 | 'other_component_id': otherComponentId, 38 | }; 39 | } 40 | 41 | class ConnectionOut extends Connection { 42 | /// Connection type that is saved to source component [connection]. 43 | ConnectionOut({ 44 | required super.connectionId, 45 | required super.otherComponentId, 46 | }); 47 | } 48 | 49 | class ConnectionIn extends Connection { 50 | /// Connection type that is saved to target component [connection]. 51 | ConnectionIn({ 52 | required super.connectionId, 53 | required super.otherComponentId, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/canvas_context/model/diagram_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/diagram_editor.dart'; 2 | 3 | class DiagramData { 4 | final List components; 5 | final List links; 6 | 7 | /// Contains list of all components and list of all links of the diagram 8 | DiagramData({ 9 | required this.components, 10 | required this.links, 11 | }); 12 | 13 | DiagramData.fromJson( 14 | Map json, { 15 | Function(Map json)? decodeCustomComponentData, 16 | Function(Map json)? decodeCustomLinkData, 17 | }) : components = (json['components'] as List) 18 | .map( 19 | (componentJson) => ComponentData.fromJson( 20 | componentJson, 21 | decodeCustomComponentData: decodeCustomComponentData, 22 | ), 23 | ) 24 | .toList(), 25 | links = (json['links'] as List) 26 | .map( 27 | (linkJson) => LinkData.fromJson( 28 | linkJson, 29 | decodeCustomLinkData: decodeCustomLinkData, 30 | ), 31 | ) 32 | .toList(); 33 | 34 | Map toJson() => { 35 | 'components': components, 36 | 'links': links, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/canvas_context/model/link_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/utils/link_style.dart'; 2 | import 'package:diagram_editor/src/utils/vector_utils.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Class that carries all link data. 6 | class LinkData with ChangeNotifier { 7 | /// Unique link id. 8 | final String id; 9 | 10 | /// Id of source component. 11 | final String sourceComponentId; 12 | 13 | /// Id of target component. 14 | final String targetComponentId; 15 | 16 | /// Defines link design such as color, width and arrowheads. 17 | final LinkStyle linkStyle; 18 | 19 | /// Points from which the link is drawn on canvas. 20 | /// 21 | /// First and last points lay on the two components which are connected by this link. 22 | final List linkPoints; 23 | 24 | /// Defines the visibility of link's joints. 25 | bool areJointsVisible = false; 26 | 27 | /// Dynamic data for you to define your own data for this link. 28 | dynamic data; 29 | 30 | /// Represents data of a link/connection in the model. 31 | LinkData({ 32 | required this.id, 33 | required this.sourceComponentId, 34 | required this.targetComponentId, 35 | LinkStyle? linkStyle, 36 | required this.linkPoints, 37 | this.data, 38 | }) : linkStyle = linkStyle ?? LinkStyle(); 39 | 40 | /// Updates this link on the canvas. 41 | /// 42 | /// Use this function if you somehow changed the link data and you want to propagate the change to canvas. 43 | /// Usually this is already called in most functions such as [setStart] or [insertMiddlePoint] so it's not necessary to call it again. 44 | /// 45 | /// It calls [notifyListeners] function of [ChangeNotifier]. 46 | void updateLink() { 47 | notifyListeners(); 48 | } 49 | 50 | /// Sets the position of the first point of the link which lies on the source component. 51 | void setStart(Offset start) { 52 | linkPoints[0] = start; 53 | notifyListeners(); 54 | } 55 | 56 | /// Sets the position of the last point of the link which lies on the target component. 57 | void setEnd(Offset end) { 58 | linkPoints[linkPoints.length - 1] = end; 59 | notifyListeners(); 60 | } 61 | 62 | /// Sets the position of both first and last point of the link. 63 | /// 64 | /// The points lie on the source and target components. 65 | void setEndpoints(Offset start, Offset end) { 66 | linkPoints[0] = start; 67 | linkPoints[linkPoints.length - 1] = end; 68 | notifyListeners(); 69 | } 70 | 71 | /// Returns list of all point of the link. 72 | List getLinkPoints() { 73 | return linkPoints; 74 | } 75 | 76 | /// Adds a new point to link on [point] location. 77 | /// 78 | /// [index] is an index of link's segment where you want to insert the point. 79 | /// Indexed from 1. 80 | /// When the link is a straight line you want to add a point to index 1. 81 | void insertMiddlePoint(Offset position, int index) { 82 | assert(index > 0); 83 | assert(index < linkPoints.length); 84 | linkPoints.insert(index, position); 85 | notifyListeners(); 86 | } 87 | 88 | /// Sets the new position ([point]) to the existing link's point. 89 | /// 90 | /// Middle points are indexed from 1. 91 | void setMiddlePointPosition(Offset position, int index) { 92 | linkPoints[index] = position; 93 | notifyListeners(); 94 | } 95 | 96 | /// Updates link's point position by [offset]. 97 | /// 98 | /// Middle points are indexed from 1. 99 | void moveMiddlePoint(Offset offset, int index) { 100 | linkPoints[index] += offset; 101 | notifyListeners(); 102 | } 103 | 104 | /// Removes the point on [index]^th place from the link. 105 | /// 106 | /// Middle points are indexed from 1. 107 | void removeMiddlePoint(int index) { 108 | assert(linkPoints.length > 2); 109 | assert(index > 0); 110 | assert(index < linkPoints.length - 1); 111 | linkPoints.removeAt(index); 112 | notifyListeners(); 113 | } 114 | 115 | /// Updates all link's middle points position by [offset]. 116 | void moveAllMiddlePoints(Offset position) { 117 | for (int i = 1; i < linkPoints.length - 1; i++) { 118 | linkPoints[i] += position; 119 | } 120 | } 121 | 122 | /// If a link is compound from more than one segments this returns an index of the link segment, which was tapped on. 123 | /// 124 | /// Segments are indexed from 1. 125 | /// If there is no link segment on the tap location it returns null. 126 | /// It should take a [localPosition] from a [onLinkTap] function or similar. 127 | int? determineLinkSegmentIndex( 128 | Offset position, 129 | Offset canvasPosition, 130 | double canvasScale, 131 | ) { 132 | for (int i = 0; i < linkPoints.length - 1; i++) { 133 | var point1 = linkPoints[i] * canvasScale + canvasPosition; 134 | var point2 = linkPoints[i + 1] * canvasScale + canvasPosition; 135 | 136 | Path rect = VectorUtils.getRectAroundLine( 137 | point1, 138 | point2, 139 | canvasScale * (linkStyle.lineWidth + 5), 140 | ); 141 | 142 | if (rect.contains(position)) { 143 | return i + 1; 144 | } 145 | } 146 | return null; 147 | } 148 | 149 | /// Makes all link's joint visible. 150 | void showJoints() { 151 | areJointsVisible = true; 152 | notifyListeners(); 153 | } 154 | 155 | /// Hides all link's joint. 156 | void hideJoints() { 157 | areJointsVisible = false; 158 | notifyListeners(); 159 | } 160 | 161 | LinkData.fromJson( 162 | Map json, { 163 | Function(Map json)? decodeCustomLinkData, 164 | }) : id = json['id'], 165 | sourceComponentId = json['source_component_id'], 166 | targetComponentId = json['target_component_id'], 167 | linkStyle = LinkStyle.fromJson(json['link_style']), 168 | linkPoints = (json['link_points'] as List) 169 | .map((point) => Offset(point[0], point[1])) 170 | .toList(), 171 | data = decodeCustomLinkData?.call(json['dynamic_data']); 172 | 173 | Map toJson() => { 174 | 'id': id, 175 | 'source_component_id': sourceComponentId, 176 | 'target_component_id': targetComponentId, 177 | 'link_style': linkStyle, 178 | 'link_points': linkPoints.map((point) => [point.dx, point.dy]).toList(), 179 | 'dynamic_data': data?.toJson(), 180 | }; 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/utils/link_style.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:diagram_editor/src/utils/vector_utils.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | enum ArrowType { 7 | none, 8 | arrow, 9 | pointedArrow, 10 | circle, 11 | centerCircle, 12 | semiCircle, 13 | } 14 | 15 | enum LineType { 16 | solid, 17 | dashed, 18 | dotted, 19 | } 20 | 21 | class LinkStyle { 22 | /// Defines the design of the link's line. 23 | /// 24 | /// It can be [LineType.solid], [LineType.dashed] or [LineType.dotted]. 25 | LineType lineType; 26 | 27 | /// Defines the design of the link's front arrowhead. 28 | /// 29 | /// There are several designs, choose from [ArrowType] enum. 30 | ArrowType arrowType; 31 | 32 | /// Defines the design of the link's back arrowhead. 33 | /// 34 | /// There are several designs, choose from [ArrowType] enum. 35 | ArrowType backArrowType; 36 | 37 | /// Defines the size of the link's front arrowhead. 38 | double arrowSize; 39 | 40 | /// Defines the size of the link's back arrowhead. 41 | double backArrowSize; 42 | 43 | /// Defines the width of the link's line. 44 | double lineWidth; 45 | 46 | /// Defines the color of the link's line and both arrowheads. 47 | Color color; 48 | 49 | /// Defines a visual design of a link on the canvas. 50 | LinkStyle({ 51 | this.lineType = LineType.solid, 52 | this.arrowType = ArrowType.none, 53 | this.backArrowType = ArrowType.none, 54 | this.arrowSize = 5, 55 | this.backArrowSize = 5, 56 | this.lineWidth = 1, 57 | this.color = Colors.black, 58 | }) : assert(lineWidth > 0), 59 | assert(arrowSize > 0); 60 | 61 | Path getArrowTipPath( 62 | ArrowType arrowType, 63 | double arrowSize, 64 | Offset point1, 65 | Offset point2, 66 | double scale, 67 | ) { 68 | switch (arrowType) { 69 | case ArrowType.none: 70 | return Path(); 71 | case ArrowType.arrow: 72 | return getArrowPath(arrowSize, point1, point2, scale, 1); 73 | case ArrowType.pointedArrow: 74 | return getArrowPath(arrowSize, point1, point2, scale, 2); 75 | case ArrowType.circle: 76 | return getCirclePath(arrowSize, point1, point2, scale, false); 77 | case ArrowType.centerCircle: 78 | return getCirclePath(arrowSize, point1, point2, scale, true); 79 | case ArrowType.semiCircle: 80 | return getSemiCirclePath(arrowSize, point1, point2, scale); 81 | } 82 | } 83 | 84 | Path getLinePath(Offset point1, Offset point2, double scale) { 85 | switch (lineType) { 86 | case LineType.solid: 87 | return getSolidLinePath(point1, point2); 88 | case LineType.dashed: 89 | return getDashedLinePath(point1, point2, scale, 16, 16); 90 | case LineType.dotted: 91 | return getDashedLinePath( 92 | point1, 93 | point2, 94 | scale, 95 | lineWidth, 96 | lineWidth * 5, 97 | ); 98 | } 99 | } 100 | 101 | Path getArrowPath( 102 | double arrowSize, 103 | Offset point1, 104 | Offset point2, 105 | double scale, 106 | double pointed, 107 | ) { 108 | Offset left = point2 + 109 | VectorUtils.normalizeVector( 110 | VectorUtils.getPerpendicularVector(point1, point2), 111 | ) * 112 | arrowSize * 113 | scale - 114 | VectorUtils.normalizeVector( 115 | VectorUtils.getDirectionVector(point1, point2), 116 | ) * 117 | pointed * 118 | arrowSize * 119 | scale; 120 | Offset right = point2 - 121 | VectorUtils.normalizeVector( 122 | VectorUtils.getPerpendicularVector(point1, point2), 123 | ) * 124 | arrowSize * 125 | scale - 126 | VectorUtils.normalizeVector( 127 | VectorUtils.getDirectionVector(point1, point2), 128 | ) * 129 | pointed * 130 | arrowSize * 131 | scale; 132 | 133 | Path path = Path(); 134 | 135 | path.moveTo(point2.dx, point2.dy); 136 | path.lineTo(left.dx, left.dy); 137 | path.lineTo(right.dx, right.dy); 138 | path.close(); 139 | 140 | return path; 141 | } 142 | 143 | Path getCirclePath( 144 | double arrowSize, 145 | Offset point1, 146 | Offset point2, 147 | double scale, 148 | bool isCenter, 149 | ) { 150 | Path path = Path(); 151 | if (isCenter) { 152 | path.addOval(Rect.fromCircle(center: point2, radius: scale * arrowSize)); 153 | } else { 154 | Offset circleCenter = point2 - 155 | VectorUtils.normalizeVector( 156 | VectorUtils.getDirectionVector(point1, point2), 157 | ) * 158 | arrowSize * 159 | scale; 160 | path.addOval( 161 | Rect.fromCircle(center: circleCenter, radius: scale * arrowSize), 162 | ); 163 | } 164 | return path; 165 | } 166 | 167 | Path getSemiCirclePath( 168 | double arrowSize, 169 | Offset point1, 170 | Offset point2, 171 | double scale, 172 | ) { 173 | Path path = Path(); 174 | Offset circleCenter = point2 - 175 | VectorUtils.normalizeVector( 176 | VectorUtils.getDirectionVector(point1, point2), 177 | ) * 178 | arrowSize * 179 | scale; 180 | path.addArc( 181 | Rect.fromCircle(center: circleCenter, radius: scale * arrowSize), 182 | math.pi - math.atan2(point2.dx - point1.dx, point2.dy - point1.dy), 183 | -math.pi, 184 | ); 185 | return path; 186 | } 187 | 188 | double getEndShortening(ArrowType arrowType) { 189 | double eps = 0.05; 190 | switch (arrowType) { 191 | case ArrowType.none: 192 | return 0; 193 | case ArrowType.arrow: 194 | return arrowSize - eps; 195 | case ArrowType.pointedArrow: 196 | return (arrowSize * 2) - eps; 197 | case ArrowType.circle: 198 | return arrowSize - eps; 199 | case ArrowType.centerCircle: 200 | return 0; 201 | case ArrowType.semiCircle: 202 | return arrowSize - eps; 203 | } 204 | } 205 | 206 | Path getSolidLinePath(Offset point1, Offset point2) { 207 | Path path = Path(); 208 | path.moveTo(point1.dx, point1.dy); 209 | path.lineTo(point2.dx, point2.dy); 210 | return path; 211 | } 212 | 213 | Path getDashedLinePath( 214 | Offset point1, 215 | Offset point2, 216 | double scale, 217 | double dashLength, 218 | double dashSpace, 219 | ) { 220 | Path path = Path(); 221 | 222 | Offset normalized = VectorUtils.normalizeVector( 223 | VectorUtils.getDirectionVector(point1, point2), 224 | ); 225 | double lineDistance = (point2 - point1).distance; 226 | Offset currentPoint = Offset(point1.dx, point1.dy); 227 | 228 | double dash = dashLength * scale; 229 | double space = dashSpace * scale; 230 | double currentDistance = 0; 231 | while (currentDistance < lineDistance) { 232 | path.moveTo(currentPoint.dx, currentPoint.dy); 233 | currentPoint = currentPoint + normalized * dash; 234 | 235 | if (currentDistance + dash > lineDistance) { 236 | path.lineTo(point2.dx, point2.dy); 237 | } else { 238 | path.lineTo(currentPoint.dx, currentPoint.dy); 239 | } 240 | currentPoint = currentPoint + normalized * space; 241 | 242 | currentDistance += dash + space; 243 | } 244 | 245 | path.moveTo( 246 | point2.dx - normalized.dx * lineWidth * scale, 247 | point2.dy - normalized.dy * lineWidth * scale, 248 | ); 249 | path.lineTo(point2.dx, point2.dy); 250 | return path; 251 | } 252 | 253 | LinkStyle.fromJson(Map json) 254 | : lineType = LineType.values[json['line_type']], 255 | arrowType = ArrowType.values[json['arrow_type']], 256 | backArrowType = ArrowType.values[json['back_arrow_type']], 257 | arrowSize = json['arrow_size'], 258 | backArrowSize = json['back_arrow_size'], 259 | lineWidth = json['line_width'], 260 | color = Color(int.parse(json['color'], radix: 16)); 261 | 262 | Map toJson() => { 263 | 'line_type': lineType.index, 264 | 'arrow_type': arrowType.index, 265 | 'back_arrow_type': backArrowType.index, 266 | 'arrow_size': arrowSize, 267 | 'back_arrow_size': backArrowSize, 268 | 'line_width': lineWidth, 269 | 'color': (((color.a * 255).round() << 24) | 270 | ((color.r * 255).round() << 16) | 271 | ((color.g * 255).round() << 8) | 272 | ((color.b * 255).round())) 273 | .toRadixString(16), 274 | }; 275 | } 276 | -------------------------------------------------------------------------------- /lib/src/utils/painter/delete_icon_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DeleteIconPainter extends CustomPainter { 4 | final Offset location; 5 | final double radius; 6 | final Color color; 7 | 8 | DeleteIconPainter({ 9 | required this.location, 10 | required this.radius, 11 | required this.color, 12 | }) : assert(radius > 0); 13 | 14 | @override 15 | void paint(Canvas canvas, Size size) { 16 | var paint = Paint() 17 | ..color = Colors.white.withValues(alpha: 0.8) 18 | ..style = PaintingStyle.fill; 19 | 20 | canvas.drawCircle(location, radius, paint); 21 | 22 | paint 23 | ..style = PaintingStyle.stroke 24 | ..color = Colors.grey 25 | ..strokeWidth = 2; 26 | 27 | canvas.drawCircle(location, radius, paint); 28 | 29 | paint.color = color; 30 | 31 | var halfRadius = radius / 2; 32 | canvas.drawLine( 33 | location + Offset(-halfRadius, -halfRadius), 34 | location + Offset(halfRadius, halfRadius), 35 | paint, 36 | ); 37 | 38 | canvas.drawLine( 39 | location + Offset(halfRadius, -halfRadius), 40 | location + Offset(-halfRadius, halfRadius), 41 | paint, 42 | ); 43 | } 44 | 45 | @override 46 | bool shouldRepaint(CustomPainter oldDelegate) => true; 47 | 48 | @override 49 | bool hitTest(Offset position) { 50 | Path path = Path(); 51 | path.addOval( 52 | Rect.fromCircle( 53 | center: location, 54 | radius: radius, 55 | ), 56 | ); 57 | 58 | return path.contains(position); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/utils/painter/grid_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridPainter extends CustomPainter { 4 | final double lineWidth; 5 | final Color lineColor; 6 | 7 | final double horizontalGap; 8 | final double verticalGap; 9 | 10 | final Offset offset; 11 | final double scale; 12 | 13 | final bool showHorizontal; 14 | final bool showVertical; 15 | 16 | final double lineLength; 17 | 18 | final bool isAntiAlias; 19 | final bool matchParentSize; 20 | 21 | /// Paints a grid. 22 | /// 23 | /// Useful if added as canvas background widget. 24 | GridPainter({ 25 | this.lineWidth = 1.0, 26 | this.lineColor = Colors.black, 27 | this.horizontalGap = 32.0, 28 | this.verticalGap = 32.0, 29 | this.offset = Offset.zero, 30 | this.scale = 1.0, 31 | this.showHorizontal = true, 32 | this.showVertical = true, 33 | this.lineLength = 1e4, 34 | this.isAntiAlias = true, 35 | this.matchParentSize = true, 36 | }); 37 | 38 | @override 39 | void paint(Canvas canvas, Size size) { 40 | late double lineHorizontalLength; 41 | late double lineVerticalLength; 42 | if (matchParentSize) { 43 | lineHorizontalLength = size.width / scale; 44 | lineVerticalLength = size.height / scale; 45 | } else { 46 | lineHorizontalLength = lineLength / scale; 47 | lineVerticalLength = lineLength / scale; 48 | } 49 | 50 | var paint = Paint() 51 | ..style = PaintingStyle.stroke 52 | ..isAntiAlias = isAntiAlias 53 | ..color = lineColor 54 | ..strokeWidth = lineWidth; 55 | 56 | if (showHorizontal) { 57 | var count = (lineVerticalLength / horizontalGap).round(); 58 | for (int i = -count + 1; i < count; i++) { 59 | canvas.drawLine( 60 | (Offset(-lineHorizontalLength, i * horizontalGap) + 61 | offset % horizontalGap) * 62 | scale, 63 | (Offset(lineHorizontalLength, i * horizontalGap) + 64 | offset % horizontalGap) * 65 | scale, 66 | paint, 67 | ); 68 | } 69 | } 70 | 71 | if (showVertical) { 72 | var count = (lineHorizontalLength / verticalGap).round(); 73 | for (int i = -count + 1; i < count; i++) { 74 | canvas.drawLine( 75 | (Offset(i * verticalGap, -lineVerticalLength) + 76 | offset % verticalGap) * 77 | scale, 78 | (Offset(i * verticalGap, lineVerticalLength) + offset % verticalGap) * 79 | scale, 80 | paint, 81 | ); 82 | } 83 | } 84 | } 85 | 86 | @override 87 | bool shouldRepaint(CustomPainter oldDelegate) { 88 | if (oldDelegate is GridPainter) { 89 | return oldDelegate.lineWidth != lineWidth || 90 | oldDelegate.lineColor != lineColor || 91 | oldDelegate.horizontalGap != horizontalGap || 92 | oldDelegate.verticalGap != verticalGap || 93 | oldDelegate.offset != offset || 94 | oldDelegate.scale != scale || 95 | oldDelegate.showHorizontal != showHorizontal || 96 | oldDelegate.showVertical != showVertical || 97 | oldDelegate.lineLength != lineLength || 98 | oldDelegate.isAntiAlias != isAntiAlias || 99 | oldDelegate.matchParentSize != matchParentSize; 100 | } 101 | return true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/utils/painter/link_joint_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LinkJointPainter extends CustomPainter { 4 | final Offset location; 5 | final double radius; 6 | final double scale; 7 | final Color color; 8 | 9 | LinkJointPainter({ 10 | required this.location, 11 | required this.radius, 12 | required this.scale, 13 | required this.color, 14 | }) : assert(radius > 0); 15 | 16 | @override 17 | void paint(Canvas canvas, Size size) { 18 | var paint = Paint() 19 | ..color = color 20 | ..style = PaintingStyle.fill; 21 | 22 | canvas.drawCircle(location, scale * radius, paint); 23 | } 24 | 25 | @override 26 | bool shouldRepaint(CustomPainter oldDelegate) => true; 27 | 28 | @override 29 | bool hitTest(Offset position) { 30 | Path path = Path(); 31 | path.addOval( 32 | Rect.fromCircle( 33 | center: location, 34 | radius: scale * radius, 35 | ), 36 | ); 37 | 38 | return path.contains(position); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/utils/painter/link_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/utils/link_style.dart'; 2 | import 'package:diagram_editor/src/utils/vector_utils.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class LinkPainter extends CustomPainter { 6 | final List linkPoints; 7 | final double scale; 8 | final LinkStyle linkStyle; 9 | 10 | LinkPainter({ 11 | required this.linkPoints, 12 | required this.scale, 13 | required this.linkStyle, 14 | }); 15 | 16 | @override 17 | void paint(Canvas canvas, Size size) { 18 | var paint = Paint() 19 | ..color = linkStyle.color 20 | ..strokeWidth = linkStyle.lineWidth * scale 21 | ..style = PaintingStyle.stroke; 22 | 23 | for (int i = 0; i < linkPoints.length - 1; i++) { 24 | if (linkPoints.length == 2) { 25 | canvas.drawPath( 26 | linkStyle.getLinePath( 27 | VectorUtils.getShorterLineStart( 28 | linkPoints[i], 29 | linkPoints[i + 1], 30 | scale * linkStyle.getEndShortening(linkStyle.backArrowType), 31 | ), 32 | VectorUtils.getShorterLineEnd( 33 | linkPoints[i], 34 | linkPoints[i + 1], 35 | scale * linkStyle.getEndShortening(linkStyle.arrowType), 36 | ), 37 | scale, 38 | ), 39 | paint, 40 | ); 41 | } else if (i == 0) { 42 | canvas.drawPath( 43 | linkStyle.getLinePath( 44 | VectorUtils.getShorterLineStart( 45 | linkPoints[i], 46 | linkPoints[i + 1], 47 | scale * linkStyle.getEndShortening(linkStyle.backArrowType), 48 | ), 49 | linkPoints[i + 1], 50 | scale, 51 | ), 52 | paint, 53 | ); 54 | } else if (i == linkPoints.length - 2) { 55 | canvas.drawPath( 56 | linkStyle.getLinePath( 57 | linkPoints[i], 58 | VectorUtils.getShorterLineEnd( 59 | linkPoints[i], 60 | linkPoints[i + 1], 61 | scale * linkStyle.getEndShortening(linkStyle.arrowType), 62 | ), 63 | scale, 64 | ), 65 | paint, 66 | ); 67 | } else { 68 | canvas.drawPath( 69 | linkStyle.getLinePath(linkPoints[i], linkPoints[i + 1], scale), 70 | paint); 71 | } 72 | } 73 | 74 | paint.style = PaintingStyle.fill; 75 | canvas.drawPath( 76 | linkStyle.getArrowTipPath( 77 | linkStyle.arrowType, 78 | linkStyle.arrowSize, 79 | linkPoints[linkPoints.length - 2], 80 | linkPoints[linkPoints.length - 1], 81 | scale, 82 | ), 83 | paint, 84 | ); 85 | 86 | canvas.drawPath( 87 | linkStyle.getArrowTipPath( 88 | linkStyle.backArrowType, 89 | linkStyle.backArrowSize, 90 | linkPoints[1], 91 | linkPoints[0], 92 | scale, 93 | ), 94 | paint, 95 | ); 96 | 97 | // DEBUG: 98 | // paint 99 | // ..color = Colors.green 100 | // ..style = PaintingStyle.stroke 101 | // ..strokeWidth = scale * 0.2; 102 | // canvas.drawPath( 103 | // makeWiderLinePath(scale * (5 + linkStyle.lineWidth)), paint); 104 | } 105 | 106 | @override 107 | bool shouldRepaint(CustomPainter oldDelegate) => true; 108 | 109 | @override 110 | bool hitTest(Offset position) { 111 | Path path = makeWiderLinePath(scale * (5 + linkStyle.lineWidth)); 112 | return path.contains(position); 113 | } 114 | 115 | Path makeWiderLinePath(double hitAreaWidth) { 116 | Path path = Path(); 117 | for (int i = 0; i < linkPoints.length - 1; i++) { 118 | var point1 = linkPoints[i]; 119 | var point2 = linkPoints[i + 1]; 120 | 121 | // DEBUG: 122 | // if (i == 0) 123 | // point1 = PainterUtils.getShorterLineStart(point1, point2, scale * 10); 124 | // if (i == linkPoints.length - 2) 125 | // point2 = PainterUtils.getShorterLineEnd(point1, point2, scale * 10); 126 | 127 | path.addPath(VectorUtils.getRectAroundLine(point1, point2, hitAreaWidth), 128 | const Offset(0, 0)); 129 | } 130 | return path; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/src/utils/painter/rect_highlight_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ComponentHighlightPainter extends CustomPainter { 4 | final double width; 5 | final double height; 6 | final Color color; 7 | final double strokeWidth; 8 | final double dashWidth; 9 | final double dashSpace; 10 | 11 | /// Rectangular dashed line painter. 12 | /// 13 | /// Useful if added as component widget to highlight it. 14 | ComponentHighlightPainter({ 15 | required this.width, 16 | required this.height, 17 | this.color = Colors.red, 18 | this.strokeWidth = 2, 19 | this.dashWidth = 10, 20 | this.dashSpace = 5, 21 | }); 22 | 23 | @override 24 | void paint(Canvas canvas, Size size) { 25 | var paint = Paint() 26 | ..color = color 27 | ..strokeWidth = strokeWidth 28 | ..style = PaintingStyle.stroke; 29 | 30 | if (dashWidth <= 0 || dashSpace <= 0) { 31 | canvas.drawRect( 32 | Rect.fromLTWH( 33 | 0, 34 | 0, 35 | this.width, 36 | this.height, 37 | ), 38 | paint, 39 | ); 40 | return; 41 | } 42 | 43 | Path dashedPath = Path(); 44 | 45 | var width = this.width + strokeWidth; 46 | var height = this.height + strokeWidth; 47 | 48 | var position = Offset(-strokeWidth / 2, 0); 49 | double pathLength = 0; 50 | 51 | if (dashWidth + 2 * dashSpace >= width) { 52 | dashedPath.moveTo(position.dx, position.dy); 53 | dashedPath.lineTo(position.dx + width, position.dy); 54 | 55 | dashedPath.moveTo(position.dx, this.height + position.dy); 56 | dashedPath.lineTo(position.dx + width, this.height + position.dy); 57 | } else { 58 | while (pathLength < width) { 59 | double nextX = (pathLength + dashWidth < width) 60 | ? position.dx + pathLength + dashWidth 61 | : position.dx + width; 62 | dashedPath.moveTo(position.dx + pathLength, position.dy); 63 | dashedPath.lineTo(nextX, position.dy); 64 | 65 | dashedPath.moveTo(position.dx + pathLength, this.height + position.dy); 66 | dashedPath.lineTo(nextX, this.height + position.dy); 67 | 68 | pathLength = pathLength + dashWidth + dashSpace; 69 | } 70 | } 71 | 72 | position = Offset(0, -strokeWidth / 2); 73 | pathLength = 0; 74 | 75 | if (dashWidth + 2 * dashSpace >= height) { 76 | dashedPath.moveTo(position.dx, position.dy); 77 | dashedPath.lineTo(position.dx, height + position.dy); 78 | 79 | dashedPath.moveTo(this.width + position.dx, position.dy); 80 | dashedPath.lineTo(this.width + position.dx, height + position.dy); 81 | } else { 82 | while (pathLength < height) { 83 | double nextY = (pathLength + dashWidth < height) 84 | ? position.dy + pathLength + dashWidth 85 | : position.dy + height; 86 | 87 | dashedPath.moveTo(position.dx, position.dy + pathLength); 88 | dashedPath.lineTo(position.dx, nextY); 89 | 90 | dashedPath.moveTo(this.width + position.dx, position.dy + pathLength); 91 | dashedPath.lineTo(this.width + position.dx, nextY); 92 | 93 | pathLength = pathLength + dashWidth + dashSpace; 94 | } 95 | } 96 | 97 | canvas.drawPath(dashedPath, paint); 98 | } 99 | 100 | @override 101 | bool shouldRepaint(CustomPainter oldDelegate) => true; 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/utils/vector_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Simple class with static methods for computing basic vector operation. 4 | /// It uses Offset as 2D vector. 5 | class VectorUtils { 6 | static Offset getDirectionVector(Offset point1, Offset point2) { 7 | return point2 - point1; 8 | } 9 | 10 | static Offset getPerpendicularVector(Offset point11, Offset point2) { 11 | return Offset((point2.dy - point11.dy), -(point2.dx - point11.dx)); 12 | } 13 | 14 | static Offset getPerpendicularVectorToVector( 15 | Offset vector, [ 16 | bool clockwise = true, 17 | ]) { 18 | return clockwise 19 | ? Offset(-vector.dy, vector.dx) 20 | : Offset(vector.dy, -vector.dx); 21 | } 22 | 23 | static Offset normalizeVector(Offset vector) { 24 | return vector.distance == 0.0 ? vector : vector / vector.distance; 25 | } 26 | 27 | static Offset getShorterLineStart( 28 | Offset point1, 29 | Offset point2, 30 | double shortening, 31 | ) { 32 | return point1 + 33 | normalizeVector(getDirectionVector(point1, point2)) * shortening; 34 | } 35 | 36 | static Offset getShorterLineEnd( 37 | Offset point1, 38 | Offset point2, 39 | double shortening, 40 | ) { 41 | return point2 - 42 | normalizeVector(getDirectionVector(point1, point2)) * shortening; 43 | } 44 | 45 | static Path getRectAroundLine(Offset point1, Offset point2, rectWidth) { 46 | Path path = Path(); 47 | Offset pnsv = VectorUtils.normalizeVector( 48 | VectorUtils.getPerpendicularVector(point1, point2), 49 | ) * 50 | rectWidth; 51 | 52 | // rect around line 53 | path.moveTo(point1.dx + pnsv.dx, point1.dy + pnsv.dy); 54 | path.lineTo(point2.dx + pnsv.dx, point2.dy + pnsv.dy); 55 | path.lineTo(point2.dx - pnsv.dx, point2.dy - pnsv.dy); 56 | path.lineTo(point1.dx - pnsv.dx, point1.dy - pnsv.dy); 57 | path.close(); 58 | 59 | return path; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/widget/canvas.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/abstraction_layer/policy/defaults/canvas_control_policy.dart'; 3 | import 'package:diagram_editor/src/canvas_context/canvas_model.dart'; 4 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 5 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 6 | import 'package:diagram_editor/src/canvas_context/model/link_data.dart'; 7 | import 'package:diagram_editor/src/widget/component.dart'; 8 | import 'package:diagram_editor/src/widget/link.dart'; 9 | import 'package:flutter/gestures.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | class DiagramEditorCanvas extends StatefulWidget { 14 | final PolicySet policy; 15 | 16 | /// The canvas where all components and links are shown on. 17 | const DiagramEditorCanvas({ 18 | super.key, 19 | required this.policy, 20 | }); 21 | 22 | @override 23 | DiagramEditorCanvasState createState() => DiagramEditorCanvasState(); 24 | } 25 | 26 | class DiagramEditorCanvasState extends State 27 | with TickerProviderStateMixin { 28 | PolicySet? withControlPolicy; 29 | 30 | @override 31 | void initState() { 32 | withControlPolicy = (widget.policy is CanvasControlPolicy || 33 | widget.policy is CanvasMovePolicy) 34 | ? widget.policy 35 | : null; 36 | 37 | (withControlPolicy as CanvasControlPolicy?)?.setAnimationController( 38 | AnimationController( 39 | duration: const Duration(seconds: 1), 40 | vsync: this, 41 | ), 42 | ); 43 | super.initState(); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | (withControlPolicy as CanvasControlPolicy?)?.disposeAnimationController(); 49 | super.dispose(); 50 | } 51 | 52 | List showComponents(CanvasModel canvasModel) { 53 | var zOrderedComponents = canvasModel.components.values.toList(); 54 | zOrderedComponents.sort((a, b) => a.zOrder.compareTo(b.zOrder)); 55 | 56 | return zOrderedComponents 57 | .map( 58 | (componentData) => ChangeNotifierProvider.value( 59 | value: componentData, 60 | child: Component( 61 | policy: widget.policy, 62 | ), 63 | ), 64 | ) 65 | .toList(); 66 | } 67 | 68 | List showLinks(CanvasModel canvasModel) { 69 | return canvasModel.links.values.map((LinkData linkData) { 70 | return ChangeNotifierProvider.value( 71 | value: linkData, 72 | child: Link( 73 | policy: widget.policy, 74 | ), 75 | ); 76 | }).toList(); 77 | } 78 | 79 | List showOtherWithComponentDataUnder(CanvasModel canvasModel) { 80 | return canvasModel.components.values.map((ComponentData componentData) { 81 | return ChangeNotifierProvider.value( 82 | value: componentData, 83 | builder: (context, child) { 84 | return Consumer( 85 | builder: (context, data, child) { 86 | return widget.policy 87 | .showCustomWidgetWithComponentDataUnder(context, data); 88 | }, 89 | ); 90 | }, 91 | ); 92 | }).toList(); 93 | } 94 | 95 | List showOtherWithComponentDataOver(CanvasModel canvasModel) { 96 | return canvasModel.components.values.map((ComponentData componentData) { 97 | return ChangeNotifierProvider.value( 98 | value: componentData, 99 | builder: (context, child) { 100 | return Consumer( 101 | builder: (context, data, child) { 102 | return widget.policy 103 | .showCustomWidgetWithComponentDataOver(context, data); 104 | }, 105 | ); 106 | }, 107 | ); 108 | }).toList(); 109 | } 110 | 111 | List showBackgroundWidgets() { 112 | return widget.policy.showCustomWidgetsOnCanvasBackground(context); 113 | } 114 | 115 | List showForegroundWidgets() { 116 | return widget.policy.showCustomWidgetsOnCanvasForeground(context); 117 | } 118 | 119 | Widget canvasStack(CanvasModel canvasModel) { 120 | return Stack( 121 | clipBehavior: Clip.none, 122 | fit: StackFit.expand, 123 | children: [ 124 | ...showBackgroundWidgets(), 125 | ...showOtherWithComponentDataUnder(canvasModel), 126 | if (widget.policy.showLinksOnTopOfComponents) 127 | ...showComponents(canvasModel), 128 | ...showLinks(canvasModel), 129 | if (!widget.policy.showLinksOnTopOfComponents) 130 | ...showComponents(canvasModel), 131 | ...showOtherWithComponentDataOver(canvasModel), 132 | ...showForegroundWidgets(), 133 | ], 134 | ); 135 | } 136 | 137 | Widget canvasAnimated(CanvasModel canvasModel) { 138 | final animationController = 139 | (withControlPolicy as CanvasControlPolicy).getAnimationController(); 140 | if (animationController == null) return canvasStack(canvasModel); 141 | 142 | return AnimatedBuilder( 143 | animation: animationController, 144 | builder: (BuildContext context, Widget? child) { 145 | (withControlPolicy as CanvasControlPolicy).canUpdateCanvasModel = true; 146 | return Transform( 147 | transform: Matrix4.identity() 148 | ..translate( 149 | (withControlPolicy as CanvasControlPolicy).transformPosition.dx, 150 | (withControlPolicy as CanvasControlPolicy).transformPosition.dy, 151 | ) 152 | ..scale((withControlPolicy as CanvasControlPolicy).transformScale), 153 | child: child, 154 | ); 155 | }, 156 | child: canvasStack(canvasModel), 157 | ); 158 | } 159 | 160 | @override 161 | Widget build(BuildContext context) { 162 | final canvasModel = Provider.of(context); 163 | final canvasState = Provider.of(context); 164 | 165 | return RepaintBoundary( 166 | key: canvasState.canvasGlobalKey, 167 | child: AbsorbPointer( 168 | absorbing: canvasState.shouldAbsorbPointer, 169 | child: Listener( 170 | onPointerSignal: (PointerSignalEvent event) => 171 | widget.policy.onCanvasPointerSignal(event), 172 | child: GestureDetector( 173 | child: Container( 174 | color: canvasState.color, 175 | child: ClipRect( 176 | child: (withControlPolicy != null) 177 | ? canvasAnimated(canvasModel) 178 | : canvasStack(canvasModel), 179 | ), 180 | ), 181 | onScaleStart: (details) => 182 | widget.policy.onCanvasScaleStart(details), 183 | onScaleUpdate: (details) => 184 | widget.policy.onCanvasScaleUpdate(details), 185 | onScaleEnd: (details) => widget.policy.onCanvasScaleEnd(details), 186 | onTap: () => widget.policy.onCanvasTap(), 187 | onTapDown: (TapDownDetails details) => 188 | widget.policy.onCanvasTapDown(details), 189 | onTapUp: (TapUpDetails details) => 190 | widget.policy.onCanvasTapUp(details), 191 | onTapCancel: () => widget.policy.onCanvasTapCancel(), 192 | onLongPress: () => widget.policy.onCanvasLongPress(), 193 | onLongPressStart: (LongPressStartDetails details) => 194 | widget.policy.onCanvasLongPressStart(details), 195 | onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) => 196 | widget.policy.onCanvasLongPressMoveUpdate(details), 197 | onLongPressEnd: (LongPressEndDetails details) => 198 | widget.policy.onCanvasLongPressEnd(details), 199 | onLongPressUp: () => widget.policy.onCanvasLongPressUp(), 200 | ), 201 | ), 202 | ), 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/src/widget/component.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 3 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class Component extends StatelessWidget { 9 | final PolicySet policy; 10 | 11 | /// Fundamental building unit of a diagram. Represents one component on the canvas. 12 | const Component({ 13 | super.key, 14 | required this.policy, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final componentData = Provider.of(context); 20 | final canvasState = Provider.of(context); 21 | 22 | return Positioned( 23 | left: canvasState.scale * componentData.position.dx + 24 | canvasState.position.dx, 25 | top: canvasState.scale * componentData.position.dy + 26 | canvasState.position.dy, 27 | width: canvasState.scale * componentData.size.width, 28 | height: canvasState.scale * componentData.size.height, 29 | child: Listener( 30 | onPointerSignal: (PointerSignalEvent event) { 31 | policy.onComponentPointerSignal(componentData.id, event); 32 | }, 33 | child: GestureDetector( 34 | behavior: HitTestBehavior.translucent, 35 | child: Stack( 36 | clipBehavior: Clip.none, 37 | children: [ 38 | Positioned( 39 | left: 0, 40 | top: 0, 41 | width: componentData.size.width, 42 | height: componentData.size.height, 43 | child: Container( 44 | transform: Matrix4.identity()..scale(canvasState.scale), 45 | child: policy.showComponentBody(componentData), 46 | ), 47 | ), 48 | policy.showCustomWidgetWithComponentData(context, componentData), 49 | ], 50 | ), 51 | onTap: () => policy.onComponentTap(componentData.id), 52 | onTapDown: (TapDownDetails details) => 53 | policy.onComponentTapDown(componentData.id, details), 54 | onTapUp: (TapUpDetails details) => 55 | policy.onComponentTapUp(componentData.id, details), 56 | onTapCancel: () => policy.onComponentTapCancel(componentData.id), 57 | onScaleStart: (ScaleStartDetails details) => 58 | policy.onComponentScaleStart(componentData.id, details), 59 | onScaleUpdate: (ScaleUpdateDetails details) => 60 | policy.onComponentScaleUpdate(componentData.id, details), 61 | onScaleEnd: (ScaleEndDetails details) => 62 | policy.onComponentScaleEnd(componentData.id, details), 63 | onLongPress: () => policy.onComponentLongPress(componentData.id), 64 | onLongPressStart: (LongPressStartDetails details) => 65 | policy.onComponentLongPressStart(componentData.id, details), 66 | onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) => 67 | policy.onComponentLongPressMoveUpdate(componentData.id, details), 68 | onLongPressEnd: (LongPressEndDetails details) => 69 | policy.onComponentLongPressEnd(componentData.id, details), 70 | onLongPressUp: () => policy.onComponentLongPressUp(componentData.id), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/widget/editor.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/canvas_context/canvas_model.dart'; 2 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 3 | import 'package:diagram_editor/src/canvas_context/diagram_editor_context.dart'; 4 | import 'package:diagram_editor/src/widget/canvas.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class DiagramEditor extends StatefulWidget { 9 | final DiagramEditorContext diagramEditorContext; 10 | 11 | /// The main widget of [diagram_editor] library. 12 | /// 13 | /// In this widget all the editing of a diagram happens. 14 | /// 15 | /// How to use it: [diagram_editor](https://pub.dev/packages/diagram_editor). 16 | /// 17 | /// Source code: [github](https://github.com/Arokip/fdl). 18 | /// 19 | /// It takes [DiagramEditorContext] as required parameter. 20 | /// You should define its size in its parent widget, eg. Container. 21 | const DiagramEditor({ 22 | super.key, 23 | required this.diagramEditorContext, 24 | }); 25 | 26 | @override 27 | DiagramEditorState createState() => DiagramEditorState(); 28 | } 29 | 30 | class DiagramEditorState extends State { 31 | @override 32 | void initState() { 33 | if (!widget.diagramEditorContext.canvasState.isInitialized) { 34 | widget.diagramEditorContext.policySet.initializeDiagramEditor(); 35 | widget.diagramEditorContext.canvasState.isInitialized = true; 36 | } 37 | super.initState(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return MultiProvider( 43 | providers: [ 44 | ChangeNotifierProvider.value( 45 | value: widget.diagramEditorContext.canvasModel, 46 | ), 47 | ChangeNotifierProvider.value( 48 | value: widget.diagramEditorContext.canvasState, 49 | ), 50 | ], 51 | builder: (context, child) { 52 | return DiagramEditorCanvas( 53 | policy: widget.diagramEditorContext.policySet, 54 | ); 55 | }, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/widget/link.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/canvas_state.dart'; 3 | import 'package:diagram_editor/src/canvas_context/model/link_data.dart'; 4 | import 'package:diagram_editor/src/utils/painter/link_joint_painter.dart'; 5 | import 'package:diagram_editor/src/utils/painter/link_painter.dart'; 6 | import 'package:flutter/gestures.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class Link extends StatelessWidget { 11 | final PolicySet policy; 12 | 13 | /// Widget that connects two [Component]s on the canvas. Another fundamental unit of the diagram. 14 | const Link({ 15 | super.key, 16 | required this.policy, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final linkData = Provider.of(context); 22 | final canvasState = Provider.of(context); 23 | 24 | LinkPainter linkPainter = LinkPainter( 25 | linkPoints: (linkData.linkPoints 26 | .map((point) => point * canvasState.scale + canvasState.position)) 27 | .toList(), 28 | scale: canvasState.scale, 29 | linkStyle: linkData.linkStyle, 30 | ); 31 | 32 | return Listener( 33 | onPointerSignal: (PointerSignalEvent event) => 34 | policy.onLinkPointerSignal(linkData.id, event), 35 | child: GestureDetector( 36 | child: CustomPaint( 37 | painter: linkPainter, 38 | child: Stack( 39 | fit: StackFit.expand, 40 | children: [ 41 | ...linkData.linkPoints 42 | .getRange(1, linkData.linkPoints.length - 1) 43 | .map( 44 | (jointPoint) { 45 | var index = linkData.linkPoints.indexOf(jointPoint); 46 | return Visibility( 47 | visible: linkData.areJointsVisible, 48 | child: GestureDetector( 49 | onTap: () => policy.onLinkJointTap(index, linkData.id), 50 | onTapDown: (TapDownDetails details) => policy 51 | .onLinkJointTapDown(index, linkData.id, details), 52 | onTapUp: (TapUpDetails details) => 53 | policy.onLinkJointTapUp(index, linkData.id, details), 54 | onTapCancel: () => 55 | policy.onLinkJointTapCancel(index, linkData.id), 56 | onScaleStart: (ScaleStartDetails details) => policy 57 | .onLinkJointScaleStart(index, linkData.id, details), 58 | onScaleUpdate: (ScaleUpdateDetails details) => policy 59 | .onLinkJointScaleUpdate(index, linkData.id, details), 60 | onScaleEnd: (ScaleEndDetails details) => policy 61 | .onLinkJointScaleEnd(index, linkData.id, details), 62 | onLongPress: () => 63 | policy.onLinkJointLongPress(index, linkData.id), 64 | onLongPressStart: (LongPressStartDetails details) => 65 | policy.onLinkJointLongPressStart( 66 | index, 67 | linkData.id, 68 | details, 69 | ), 70 | onLongPressMoveUpdate: 71 | (LongPressMoveUpdateDetails details) => 72 | policy.onLinkJointLongPressMoveUpdate( 73 | index, 74 | linkData.id, 75 | details, 76 | ), 77 | onLongPressEnd: (LongPressEndDetails details) => policy 78 | .onLinkJointLongPressEnd(index, linkData.id, details), 79 | onLongPressUp: () => 80 | policy.onLinkJointLongPressUp(index, linkData.id), 81 | child: CustomPaint( 82 | painter: LinkJointPainter( 83 | location: canvasState.toCanvasCoordinates(jointPoint), 84 | radius: 8, 85 | scale: canvasState.scale, 86 | color: linkData.linkStyle.color.withValues( 87 | alpha: 0.5, 88 | ), 89 | ), 90 | ), 91 | ), 92 | ); 93 | }, 94 | ), 95 | ...policy.showWidgetsWithLinkData(context, linkData), 96 | ], 97 | ), 98 | ), 99 | onTap: () => policy.onLinkTap(linkData.id), 100 | onTapDown: (TapDownDetails details) => 101 | policy.onLinkTapDown(linkData.id, details), 102 | onTapUp: (TapUpDetails details) => 103 | policy.onLinkTapUp(linkData.id, details), 104 | onTapCancel: () => policy.onLinkTapCancel(linkData.id), 105 | onScaleStart: (ScaleStartDetails details) => 106 | policy.onLinkScaleStart(linkData.id, details), 107 | onScaleUpdate: (ScaleUpdateDetails details) => 108 | policy.onLinkScaleUpdate(linkData.id, details), 109 | onScaleEnd: (ScaleEndDetails details) => 110 | policy.onLinkScaleEnd(linkData.id, details), 111 | onLongPress: () => policy.onLinkLongPress(linkData.id), 112 | onLongPressStart: (LongPressStartDetails details) => 113 | policy.onLinkLongPressStart(linkData.id, details), 114 | onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) => 115 | policy.onLinkLongPressMoveUpdate(linkData.id, details), 116 | onLongPressEnd: (LongPressEndDetails details) => 117 | policy.onLinkLongPressEnd(linkData.id, details), 118 | onLongPressUp: () => policy.onLinkLongPressUp(linkData.id), 119 | ), 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.0" 44 | crypto: 45 | dependency: transitive 46 | description: 47 | name: crypto 48 | sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "3.0.1" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | fixnum: 61 | dependency: transitive 62 | description: 63 | name: fixnum 64 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.1.0" 68 | flutter: 69 | dependency: "direct main" 70 | description: flutter 71 | source: sdk 72 | version: "0.0.0" 73 | flutter_lints: 74 | dependency: "direct dev" 75 | description: 76 | name: flutter_lints 77 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 78 | url: "https://pub.dev" 79 | source: hosted 80 | version: "5.0.0" 81 | flutter_test: 82 | dependency: "direct dev" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | leak_tracker: 87 | dependency: transitive 88 | description: 89 | name: leak_tracker 90 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "10.0.7" 94 | leak_tracker_flutter_testing: 95 | dependency: transitive 96 | description: 97 | name: leak_tracker_flutter_testing 98 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "3.0.8" 102 | leak_tracker_testing: 103 | dependency: transitive 104 | description: 105 | name: leak_tracker_testing 106 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "3.0.1" 110 | lints: 111 | dependency: transitive 112 | description: 113 | name: lints 114 | sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "5.0.0" 118 | matcher: 119 | dependency: transitive 120 | description: 121 | name: matcher 122 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "0.12.16+1" 126 | material_color_utilities: 127 | dependency: transitive 128 | description: 129 | name: material_color_utilities 130 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 131 | url: "https://pub.dev" 132 | source: hosted 133 | version: "0.11.1" 134 | meta: 135 | dependency: transitive 136 | description: 137 | name: meta 138 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 139 | url: "https://pub.dev" 140 | source: hosted 141 | version: "1.15.0" 142 | nested: 143 | dependency: transitive 144 | description: 145 | name: nested 146 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 147 | url: "https://pub.dev" 148 | source: hosted 149 | version: "1.0.0" 150 | path: 151 | dependency: transitive 152 | description: 153 | name: path 154 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 155 | url: "https://pub.dev" 156 | source: hosted 157 | version: "1.9.0" 158 | provider: 159 | dependency: "direct main" 160 | description: 161 | name: provider 162 | sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c 163 | url: "https://pub.dev" 164 | source: hosted 165 | version: "6.1.2" 166 | sky_engine: 167 | dependency: transitive 168 | description: flutter 169 | source: sdk 170 | version: "0.0.0" 171 | source_span: 172 | dependency: transitive 173 | description: 174 | name: source_span 175 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "1.10.0" 179 | sprintf: 180 | dependency: transitive 181 | description: 182 | name: sprintf 183 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "7.0.0" 187 | stack_trace: 188 | dependency: transitive 189 | description: 190 | name: stack_trace 191 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "1.12.0" 195 | stream_channel: 196 | dependency: transitive 197 | description: 198 | name: stream_channel 199 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "2.1.2" 203 | string_scanner: 204 | dependency: transitive 205 | description: 206 | name: string_scanner 207 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "1.3.0" 211 | term_glyph: 212 | dependency: transitive 213 | description: 214 | name: term_glyph 215 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "1.2.1" 219 | test_api: 220 | dependency: transitive 221 | description: 222 | name: test_api 223 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "0.7.3" 227 | typed_data: 228 | dependency: transitive 229 | description: 230 | name: typed_data 231 | sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "1.3.0" 235 | uuid: 236 | dependency: "direct main" 237 | description: 238 | name: uuid 239 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "4.5.1" 243 | vector_math: 244 | dependency: transitive 245 | description: 246 | name: vector_math 247 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "2.1.4" 251 | vm_service: 252 | dependency: transitive 253 | description: 254 | name: vm_service 255 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "14.3.0" 259 | sdks: 260 | dart: ">=3.5.0 <4.0.0" 261 | flutter: ">=3.18.0-18.0.pre.54" 262 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: diagram_editor 2 | description: A flutter diagram editor library that provides DiagramEditor widget and a possibility to customize all editor design and behavior. 3 | version: 0.2.3 4 | homepage: https://arokip.github.io/fdl_demo_app 5 | repository: https://github.com/Arokip/flutter_diagram_editor 6 | 7 | environment: 8 | sdk: ">=3.5.0 <4.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | provider: ^6.1.2 14 | uuid: ^4.5.1 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | flutter_lints: ^5.0.0 20 | -------------------------------------------------------------------------------- /test/src/canvas_context/canvas_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/diagram_editor.dart'; 2 | import 'package:diagram_editor/src/canvas_context/canvas_model.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('Canvas model tests', () { 7 | test('Given new canvas When no action Then canvas contains no components', 8 | () { 9 | PolicySet policySet = PolicySet(); 10 | var model = CanvasModel(policySet); 11 | 12 | expect(model.components.isEmpty, true); 13 | }); 14 | 15 | test( 16 | 'Given new canvas When added one component Then canvas contains one component', 17 | () { 18 | PolicySet policySet = PolicySet(); 19 | var model = CanvasModel(policySet); 20 | ComponentData componentData = ComponentData(); 21 | 22 | model.addComponent(componentData); 23 | 24 | expect(model.components.length, 1); 25 | }); 26 | 27 | test( 28 | 'Given canvas with one component When the component is removed Then canvas contains no components', 29 | () { 30 | PolicySet policySet = PolicySet(); 31 | var model = CanvasModel(policySet); 32 | ComponentData componentData = ComponentData(); 33 | 34 | String componentId = model.addComponent(componentData); 35 | 36 | model.removeComponent(componentId); 37 | 38 | expect(model.components.isEmpty, true); 39 | }); 40 | 41 | test( 42 | 'Given canvas with two components When the components are connected Then one link exists and the connections correspond', 43 | () { 44 | PolicySet policySet = PolicySet(); 45 | var model = CanvasModel(policySet); 46 | 47 | ComponentData componentDataA = ComponentData(); 48 | ComponentData componentDataB = ComponentData(); 49 | 50 | model.addComponent(componentDataA); 51 | model.addComponent(componentDataB); 52 | 53 | String linkId = model.connectTwoComponents( 54 | componentDataA.id, 55 | componentDataB.id, 56 | LinkStyle(), 57 | null, 58 | ); 59 | 60 | expect(model.links.length, 1); 61 | 62 | var connectionsA = componentDataA.connections; 63 | var connectionsB = componentDataB.connections; 64 | expect(connectionsA.length, 1); 65 | expect(connectionsB.length, 1); 66 | 67 | expect(connectionsA.single.connectionId, linkId); 68 | expect(connectionsB.single.connectionId, linkId); 69 | 70 | expect(connectionsA.single is ConnectionOut, true); 71 | expect(connectionsB.single is ConnectionIn, true); 72 | 73 | expect(model.getLink(linkId).id, linkId); 74 | }); 75 | 76 | test( 77 | 'Given canvas with two connected components When the existing link is removed Then no links exist and components have no connections', 78 | () { 79 | PolicySet policySet = PolicySet(); 80 | var model = CanvasModel(policySet); 81 | 82 | ComponentData componentDataA = ComponentData(); 83 | ComponentData componentDataB = ComponentData(); 84 | 85 | model.addComponent(componentDataA); 86 | model.addComponent(componentDataB); 87 | 88 | String linkId = model.connectTwoComponents( 89 | componentDataA.id, 90 | componentDataB.id, 91 | LinkStyle(), 92 | null, 93 | ); 94 | 95 | model.removeLink(linkId); 96 | 97 | expect(model.links.length, 0); 98 | 99 | var connectionsA = componentDataA.connections; 100 | var connectionsB = componentDataB.connections; 101 | 102 | expect(connectionsA.length, 0); 103 | expect(connectionsB.length, 0); 104 | }); 105 | 106 | test( 107 | 'Given canvas with two components connected to third one When the connection is removed on the third component Then no links exist and components have no connections', 108 | () { 109 | PolicySet policySet = PolicySet(); 110 | var model = CanvasModel(policySet); 111 | 112 | ComponentData componentDataA = ComponentData(); 113 | ComponentData componentDataB = ComponentData(); 114 | ComponentData componentDataC = ComponentData(); 115 | 116 | model.addComponent(componentDataA); 117 | model.addComponent(componentDataB); 118 | model.addComponent(componentDataC); 119 | 120 | model.connectTwoComponents( 121 | componentDataA.id, 122 | componentDataC.id, 123 | LinkStyle(), 124 | null, 125 | ); 126 | model.connectTwoComponents( 127 | componentDataC.id, 128 | componentDataB.id, 129 | LinkStyle(), 130 | null, 131 | ); 132 | 133 | model.removeComponentConnections(componentDataC.id); 134 | 135 | expect(model.links.length, 0); 136 | 137 | expect(componentDataA.connections.length, 0); 138 | expect(componentDataB.connections.length, 0); 139 | expect(componentDataC.connections.length, 0); 140 | }); 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /test/src/canvas_context/canvas_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/diagram_editor_context.dart'; 3 | import 'package:diagram_editor/src/widget/editor.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | group('Canvas state tests', () { 9 | test( 10 | 'Given new DiagramEditor When no action Then canvas position is zero and scale is 1', 11 | () { 12 | PolicySet policySet = PolicySet(); 13 | 14 | MaterialApp( 15 | home: DiagramEditor( 16 | diagramEditorContext: DiagramEditorContext( 17 | policySet: policySet, 18 | ), 19 | ), 20 | ); 21 | 22 | expect(policySet.canvasReader.state.scale, 1); 23 | expect(policySet.canvasReader.state.position, Offset.zero); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/src/canvas_context/model/component_data_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 2 | import 'package:diagram_editor/src/canvas_context/model/connection.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | group('Component data tests', () { 8 | test( 9 | 'Given ComponentData with two connections When one connection is removed Then component contains only the second connection.', 10 | () { 11 | var componentData = ComponentData(); 12 | componentData.addConnection( 13 | ConnectionIn( 14 | otherComponentId: 'componentId1', 15 | connectionId: 'connectionId1', 16 | ), 17 | ); 18 | componentData.addConnection( 19 | ConnectionIn( 20 | otherComponentId: 'componentId2', 21 | connectionId: 'connectionId2', 22 | ), 23 | ); 24 | 25 | expect(componentData.connections.length, 2); 26 | 27 | componentData.removeConnection('connectionId1'); 28 | 29 | expect(componentData.connections.length, 1); 30 | expect(componentData.connections.single.connectionId, 'connectionId2'); 31 | }); 32 | 33 | test('Point on a component test', () { 34 | var componentData = ComponentData(size: const Size(100, 100)); 35 | 36 | var alignment1 = const Alignment(0, 0); 37 | var alignment2 = const Alignment(1, 0); 38 | var alignment3 = const Alignment(-1, -1); 39 | var alignment4 = const Alignment(-0.5, 0.5); 40 | 41 | var point1 = componentData.getPointOnComponent(alignment1); 42 | var point2 = componentData.getPointOnComponent(alignment2); 43 | var point3 = componentData.getPointOnComponent(alignment3); 44 | var point4 = componentData.getPointOnComponent(alignment4); 45 | 46 | expect(point1, const Offset(50, 50)); 47 | expect(point2, const Offset(100, 50)); 48 | expect(point3, const Offset(0, 0)); 49 | expect(point4, const Offset(25, 75)); 50 | }); 51 | 52 | test('Resize component test', () { 53 | var componentData = ComponentData(size: const Size(100, 100)); 54 | 55 | componentData.resizeDelta(const Offset(10, -10)); 56 | 57 | expect(componentData.size, const Size(110, 90)); 58 | 59 | componentData.resizeDelta(const Offset(-110, -1000)); 60 | 61 | expect(componentData.size, componentData.minSize); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/src/canvas_context/model/link_data_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/canvas_context/model/link_data.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | // Tests can be run only all at once, not individually !!! 6 | group('Link data tests', () { 7 | var linkData = LinkData( 8 | id: 'id', 9 | sourceComponentId: 'sourceComponentId', 10 | targetComponentId: 'targetComponentId', 11 | linkPoints: [ 12 | const Offset(0, 0), 13 | const Offset(100, 0), 14 | ], 15 | ); 16 | 17 | test('Init linkData', () { 18 | expect(linkData.linkPoints.length, 2); 19 | expect(linkData.linkPoints, [ 20 | const Offset(0, 0), 21 | const Offset(100, 0), 22 | ]); 23 | }); 24 | 25 | test('Set start/end point of the link', () { 26 | linkData.setStart(const Offset(20, 0)); 27 | linkData.setEnd(const Offset(120, 0)); 28 | expect(linkData.linkPoints, [ 29 | const Offset(20, 0), 30 | const Offset(120, 0), 31 | ]); 32 | }); 33 | 34 | test('Insert middle point', () { 35 | linkData.insertMiddlePoint(const Offset(50, 0), 1); 36 | expect(linkData.linkPoints, [ 37 | const Offset(20, 0), 38 | const Offset(50, 0), 39 | const Offset(120, 0), 40 | ]); 41 | }); 42 | 43 | test('Set middle point position', () { 44 | linkData.setMiddlePointPosition(const Offset(70, 0), 1); 45 | expect(linkData.linkPoints, [ 46 | const Offset(20, 0), 47 | const Offset(70, 0), 48 | const Offset(120, 0), 49 | ]); 50 | }); 51 | 52 | test('Move middle point', () { 53 | linkData.moveMiddlePoint(const Offset(20, 0), 2); 54 | expect(linkData.linkPoints, [ 55 | const Offset(20, 0), 56 | const Offset(70, 0), 57 | const Offset(140, 0), 58 | ]); 59 | }); 60 | 61 | test('Update all middle points', () { 62 | linkData.insertMiddlePoint(const Offset(75, 0), 1); 63 | linkData.moveAllMiddlePoints(const Offset(10, 0)); 64 | expect(linkData.linkPoints, [ 65 | const Offset(20, 0), 66 | const Offset(85, 0), 67 | const Offset(80, 0), 68 | const Offset(140, 0), 69 | ]); 70 | }); 71 | 72 | test('Remove middle point', () { 73 | linkData.removeMiddlePoint(1); 74 | expect(linkData.linkPoints, [ 75 | const Offset(20, 0), 76 | const Offset(80, 0), 77 | const Offset(140, 0), 78 | ]); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/src/widget/canvas_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/diagram_editor.dart'; 2 | import 'package:diagram_editor/src/widget/component.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | // Tests can be run only all at once, not individually !!! 8 | group('Canvas tests', () { 9 | PolicySet policySet = PolicySet(); 10 | 11 | var editor = MaterialApp( 12 | home: DiagramEditor( 13 | diagramEditorContext: DiagramEditorContext( 14 | policySet: policySet, 15 | ), 16 | ), 17 | ); 18 | 19 | ComponentData componentData = ComponentData(); 20 | ComponentData componentData2 = ComponentData(); 21 | 22 | testWidgets( 23 | 'Given new canvas When no action Then canvas contains no components', 24 | (WidgetTester tester) async { 25 | await tester.pumpWidget(editor); 26 | expect(find.byType(Component), findsNothing); 27 | }); 28 | 29 | testWidgets( 30 | 'Given canvas with no components When component is added Then canvas contains that one component', 31 | (WidgetTester tester) async { 32 | await tester.pumpWidget(editor); 33 | 34 | policySet.canvasWriter.model.addComponent(componentData); 35 | 36 | await tester.pump(); 37 | expect(find.byType(Component), findsOneWidget); 38 | }); 39 | 40 | testWidgets( 41 | 'Given canvas with one component When component is removed Then canvas contains no components', 42 | (WidgetTester tester) async { 43 | await tester.pumpWidget(editor); 44 | 45 | expect(find.byType(Component), findsOneWidget); 46 | 47 | policySet.canvasWriter.model.removeComponent(componentData.id); 48 | 49 | await tester.pump(); 50 | expect(find.byType(Component), findsNothing); 51 | }); 52 | 53 | testWidgets( 54 | 'Given canvas with one component with a child When component is removed with children Then canvas contains no components', 55 | (WidgetTester tester) async { 56 | await tester.pumpWidget(editor); 57 | 58 | String id1 = policySet.canvasWriter.model.addComponent(componentData); 59 | String id2 = policySet.canvasWriter.model.addComponent(componentData2); 60 | policySet.canvasWriter.model.setComponentParent(id2, id1); 61 | 62 | await tester.pump(); 63 | 64 | expect(find.byType(Component), findsNWidgets(2)); 65 | 66 | policySet.canvasWriter.model.removeComponentWithChildren(id1); 67 | 68 | await tester.pump(); 69 | expect(find.byType(Component), findsNothing); 70 | }); 71 | 72 | testWidgets( 73 | 'Given canvas with one component When position is set to canvas Then canvas still contains one component', 74 | (WidgetTester tester) async { 75 | await tester.pumpWidget(editor); 76 | 77 | policySet.canvasWriter.model.addComponent(componentData); 78 | await tester.pump(); 79 | 80 | policySet.canvasWriter.state.setPosition(const Offset(10, 0)); 81 | 82 | await tester.pump(); 83 | 84 | expect(find.byType(Component), findsOneWidget); 85 | }); 86 | 87 | testWidgets( 88 | 'Given canvas with one component When canvas position is updated Then canvas still contains one component', 89 | (WidgetTester tester) async { 90 | await tester.pumpWidget(editor); 91 | 92 | policySet.canvasWriter.state.setPosition(const Offset(10, 0)); 93 | 94 | await tester.pump(); 95 | 96 | expect(find.byType(Component), findsOneWidget); 97 | }); 98 | 99 | testWidgets( 100 | 'Given canvas with one component When scale is set to canvas Then canvas still contains one component', 101 | (WidgetTester tester) async { 102 | await tester.pumpWidget(editor); 103 | 104 | policySet.canvasWriter.state.setScale(1.5); 105 | 106 | await tester.pump(); 107 | 108 | expect(find.byType(Component), findsOneWidget); 109 | }); 110 | 111 | testWidgets( 112 | 'Given canvas with one component When canvas scale is updated Then canvas still contains one component', 113 | (WidgetTester tester) async { 114 | await tester.pumpWidget(editor); 115 | 116 | policySet.canvasWriter.state.updateScale(1.5); 117 | 118 | await tester.pump(); 119 | 120 | expect(find.byType(Component), findsOneWidget); 121 | }); 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /test/src/widget/component_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/diagram_editor_context.dart'; 3 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 4 | import 'package:diagram_editor/src/widget/component.dart'; 5 | import 'package:diagram_editor/src/widget/editor.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | void main() { 10 | // Tests can be run only all at once, not individually !!! 11 | group('Component widget tests', () { 12 | PolicySet policySet = PolicySet(); 13 | 14 | var editor = MaterialApp( 15 | home: DiagramEditor( 16 | diagramEditorContext: DiagramEditorContext( 17 | policySet: policySet, 18 | ), 19 | ), 20 | ); 21 | 22 | var componentData = ComponentData( 23 | size: const Size(40, 40), 24 | position: const Offset(10, 10), 25 | ); 26 | 27 | testWidgets( 28 | 'Given one component When the component is moved Then there is still one component', 29 | (WidgetTester tester) async { 30 | await tester.pumpWidget(editor); 31 | 32 | policySet.canvasWriter.model.addComponent(componentData); 33 | 34 | await tester.pump(); 35 | 36 | expect(find.byType(Component), findsOneWidget); 37 | 38 | componentData.move(const Offset(10, 0)); 39 | 40 | await tester.pump(); 41 | 42 | expect(find.byType(Component), findsOneWidget); 43 | }); 44 | 45 | testWidgets( 46 | 'Given one component When new position is set to the component Then there is still one component', 47 | (WidgetTester tester) async { 48 | await tester.pumpWidget(editor); 49 | 50 | expect(find.byType(Component), findsOneWidget); 51 | 52 | componentData.setPosition(const Offset(0, 10)); 53 | 54 | await tester.pump(); 55 | 56 | expect(find.byType(Component), findsOneWidget); 57 | }); 58 | 59 | testWidgets( 60 | 'Given one component When the component is resized Then there is still one component', 61 | (WidgetTester tester) async { 62 | await tester.pumpWidget(editor); 63 | 64 | expect(find.byType(Component), findsOneWidget); 65 | 66 | componentData.resizeDelta(const Offset(10, 10)); 67 | 68 | await tester.pump(); 69 | 70 | expect(find.byType(Component), findsOneWidget); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/src/widget/link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:diagram_editor/src/abstraction_layer/policy/base/policy_set.dart'; 2 | import 'package:diagram_editor/src/canvas_context/diagram_editor_context.dart'; 3 | import 'package:diagram_editor/src/canvas_context/model/component_data.dart'; 4 | import 'package:diagram_editor/src/widget/component.dart'; 5 | import 'package:diagram_editor/src/widget/editor.dart'; 6 | import 'package:diagram_editor/src/widget/link.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() { 11 | // Tests can be run only all at once, not individually !!! 12 | group('Link widget tests', () { 13 | PolicySet policySet = PolicySet(); 14 | 15 | late String linkId; 16 | 17 | var editor = MaterialApp( 18 | home: DiagramEditor( 19 | diagramEditorContext: DiagramEditorContext( 20 | policySet: policySet, 21 | ), 22 | ), 23 | ); 24 | 25 | var componentData1 = ComponentData( 26 | size: const Size(40, 40), 27 | position: const Offset(20, 0), 28 | ); 29 | 30 | var componentData2 = ComponentData( 31 | size: const Size(40, 40), 32 | position: const Offset(120, 0), 33 | ); 34 | 35 | testWidgets( 36 | 'Given two components When components are connected Then a link is created', 37 | (WidgetTester tester) async { 38 | await tester.pumpWidget(editor); 39 | 40 | policySet.canvasWriter.model.addComponent(componentData1); 41 | policySet.canvasWriter.model.addComponent(componentData2); 42 | 43 | await tester.pump(); 44 | 45 | expect(find.byType(Component), findsNWidgets(2)); 46 | 47 | linkId = policySet.canvasWriter.model.connectTwoComponents( 48 | sourceComponentId: componentData1.id, 49 | targetComponentId: componentData2.id, 50 | ); 51 | 52 | await tester.pump(); 53 | 54 | expect(find.byType(Link), findsOneWidget); 55 | }); 56 | 57 | testWidgets( 58 | 'Given two connected components When link middle point is added Then there is still one link and two components', 59 | (WidgetTester tester) async { 60 | await tester.pumpWidget(editor); 61 | 62 | expect(find.byType(Component), findsNWidgets(2)); 63 | expect(find.byType(Link), findsOneWidget); 64 | 65 | policySet.canvasWriter.model 66 | .insertLinkMiddlePoint(linkId, const Offset(20, 20), 1); 67 | 68 | await tester.pump(); 69 | 70 | expect(find.byType(Component), findsNWidgets(2)); 71 | expect(find.byType(Link), findsOneWidget); 72 | }); 73 | 74 | testWidgets( 75 | 'Given two connected components with a link with middle point When link middle point is moved Then there is still one link and two components', 76 | (WidgetTester tester) async { 77 | await tester.pumpWidget(editor); 78 | 79 | expect(find.byType(Component), findsNWidgets(2)); 80 | expect(find.byType(Link), findsOneWidget); 81 | 82 | policySet.canvasWriter.model 83 | .moveLinkMiddlePoint(linkId, const Offset(20, 20), 1); 84 | 85 | await tester.pump(); 86 | 87 | expect(find.byType(Component), findsNWidgets(2)); 88 | expect(find.byType(Link), findsOneWidget); 89 | }); 90 | 91 | testWidgets( 92 | 'Given two connected components with a link with middle point When link middle point is removed Then there is still one link and two components', 93 | (WidgetTester tester) async { 94 | await tester.pumpWidget(editor); 95 | 96 | expect(find.byType(Component), findsNWidgets(2)); 97 | expect(find.byType(Link), findsOneWidget); 98 | 99 | policySet.canvasWriter.model.removeLinkMiddlePoint(linkId, 1); 100 | 101 | await tester.pump(); 102 | 103 | expect(find.byType(Component), findsNWidgets(2)); 104 | expect(find.byType(Link), findsOneWidget); 105 | }); 106 | 107 | testWidgets( 108 | 'Given two connected components When the link is removed Then there is no link and two components', 109 | (WidgetTester tester) async { 110 | await tester.pumpWidget(editor); 111 | 112 | expect(find.byType(Component), findsNWidgets(2)); 113 | 114 | policySet.canvasWriter.model.removeLink(linkId); 115 | 116 | await tester.pump(); 117 | 118 | expect(find.byType(Link), findsNothing); 119 | expect(find.byType(Component), findsNWidgets(2)); 120 | }); 121 | }); 122 | } 123 | --------------------------------------------------------------------------------