├── .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 | [](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 |
--------------------------------------------------------------------------------